大厂真实 Git 开发工作流程

一、开发分支模型分类

目前所在部门使用是主要是四种:dev(开发)、test(测试)、uat(预发)、release(生产)

小公司可能就一个 dev、一个 master 就搞定了,测试都是开发人员自己来🤣。

二、开发主体流程

  1. 需求评审
  2. 开发排期
  3. 编码开发
  4. 冒烟测试(自检验)
  5. 冒烟通过,提交测试,合并代码到测试分支,部署测试环境
  6. 测试环境测试,开发修 bug
  7. 测试完成,提交预发,合并代码到预发分支,部署预发环境
  8. 预发环境测试,开发修 bug(修完的 bug 要重新走测试再走预发,这个下面会解释)
  9. 测试完成,产品验收
  10. 验收完成,提交生产,合并代码到生产分支,部署生产环境
  11. 生产运营(客户)验收
  12. 验收完成,结项

三、具体操作

1. 拉取代码

一般都会在本地默认创建一个 master 分支

shell复制代码git clone https://code.xxx.com/xxx/xxx.git

2. 初次开发需求前,要先拉取生产/预发分支,然后基于这个分支之上,创建自己的特性分支进行开发

shell复制代码git fetch origin release:release

git checkout release

git checkout -b feat-0131-jie

此时,在你本地已经有了一个 release 分支对应着远程仓库的 release 分支,还有一个内容基于 release 分支的特性分支,之后便可以在这个特性分支上进行需求开发了。

如果不是初次开发,本地已有生产/预发分支,则需要重新拉取远程的最新代码,然后再创建

shell复制代码## 小tips:输入已有的分支名时,可以只输入前几个字符,然后按 tab 自动补充
git checkout release

git pull origin release

git checkout -b feat-0229-jie

注意1:分支名称是有规范和含义的,不能乱取。

推荐格式:分支责任-需求日期/需求号-开发人姓名,一般按部门规范来,常见的有以下几种。

js复制代码  - feat:新功能

  - fix:修补bug

  - doc:文档

  - refactor:重构(即不是新增功能,也不是修改bug的代码变动)

  - test:测试

  - chore:构建过程或辅助工具的变动

注意2:为啥拉取的是生产/预发分支

之所以要拉取 release/uat 分支而不是拉取 dev/test,是因为后者可能包含着一些其他成员还未上线或者可能有 bug 的需求代码,这些代码没有通过验证,如果被你给拉取了,然后又基于此进行新的需求开发,那当你需求开发完成,而其他成员的需求还没上线,你将会把这些未验证的代码一起发送到 uat/release 上,导致一系列问题。

3. 需求开发完成,提交&合并代码

首先先在本地把新的改动提交,提交描述的格式可以参考着分支名的格式

  • 如果是新需求的提交,可以写成 “feat: 需求0131-新增账期”
  • 如果是 bug 修复,可以写成 “fix: 禅道3387-重复请求”
shell复制代码git add .

git commit -m "提交描述"

此时,本地当前分支已经记录了你的提交记录,接下来进行代码合并了

在代码合并之前,我们先要梳理一下我们应该如何对分支进行管理(非常重要!)

  1. 首先,我们需要认知到的是,每一个分支应该只对应一个功能,例如当我们开发需求 01 时,那么就创建一个 feat-01-jie 分支进行开发;开发需求 02 时,就另外创建一个 feat-02-jie 分支进行开发;修改生产环境的某个 bug 时,就创建 fix-jie-3378 进行开发,等等。 这样做的目的是,能够把不同的功能/需求/修改分离开来。想象一下这样一个场景,如果有某些紧急的需求是需要提前上线的,而此时你的分支里既包含了这些紧急的需求,又包含了其他未开发好的需求,那么这两种需求就不能拆开来分别进行提测和上线了。
  2. 其次,在合并代码时,我们要将四种分支模型(dev、test、uat、release)作为参照物,而不是把关注点放在自己的分支上。比如我们要在 dev 上调试,那就需要把自己的分支合并到 dev 分支上;如果我们需要提测,则把自己的分支合并到 test 分支上,以此类推。 即,我们要关注到,这四个环境的分支上,会有什么内容,会新增什么内容。切记不能反过来将除了 release 之外的三个分支合并到自己的代码上!! 如果其他成员将自己的代码也提交到 dev 分支上,但是这个代码是没有通过验证的,此时你将 dev 往自己的分支上合,那之后的提测、上预发、生产则很大概率会出问题。所以一定要保持自己的分支是干净的! 而 release 分支对应的是生产环境,一般是最新的稳定版本,所以合并到自己的分支以获取最新的更改也是没什么问题的。

接下来介绍合并代码的方式:

第一种:线上合并,也是推荐的规范操作

shell复制代码git push origin feat-0131-jie

先接着上面的提交步骤,将自己的分支推送到远程仓库。

然后在线上代码仓库中,申请将自己的分支合并到 xx 分支(具体是哪个分支就根据你当前的开发进度来,如 test),然后在线上解决冲突。如果有权限就自己通过了,如果没有就得找 mt 啥的

第二种,本地合并(前提你要有对应环境分支 push 的权限)

shell复制代码## 先切换到你要提交的环境分支上,如果本地还没有就先拉取下来
git fetch origin test:test

git checkout test

## 然后将自己的分支合并到环境分支上(在编辑器解决冲突)
git merge feat-0131-jie

## 最后将环境分支推送到远程仓库
git push origin test
shell复制代码## 先切换到你要提交的环境分支上,如果本地已有该分支,则需要先拉取最新代码
git checkout test

git pull origin test

## 然后将自己的分支合并到环境分支上(在编辑器解决冲突)
git merge feat-0131-jie

## 最后将环境分支推送到远程仓库
git push origin test

两种方式有何区别?为什么推荐第一种?

这是因为在团队协作开发的过程中,将合并操作限制在线上环境有以下几个好处:

  1. 避免本地合并冲突:如果多个开发人员同时在本地进行合并操作,并且对同一段代码进行了修改,可能会导致冲突。将合并操作集中在线上环境可以减少此类冲突的发生,因为不同开发人员的修改会先在线上进行合并,然后再通过更新拉取到本地。
  2. 更好的代码审查:将合并操作放在线上环境可以方便其他开发人员进行代码审查。其他人员可以在线上查看合并请求的代码变动、注释和讨论,并提供反馈和建议。这样可以确保代码的质量和可维护性。
  3. 提高可追溯性和可回滚性:将合并操作记录在线上可以更容易地进行版本控制和管理。如果出现问题或需要回滚到之前的版本,可以更轻松地找到相关的合并记录并进行处理。

当然,并非所有情况都适用于第一种方式。在某些特定情况下,例如个人项目或小团队内部开发,允许本地合并也是可以的。但在大多数团队协作的场景中,将合并操作集中在线上环境具有更多优势。

4. 验收完成,删除分支

当我们这一版的需求完成后,本地肯定已经留有了很多分支,这些分支对于之后的开发已经意义不大了,留下来只会看着一团糟。

shell复制代码git branch -d <分支名>

## 如果要强制删除分支(即使分支上有未合并的修改)
git branch -D <分支名>

四、一些小问题

1. 前面提到,预发环境修完的 bug 要重新走测试再走预发,为什么呢?

预生产环境是介于测试和生产环境之间的一个环境,它的目的是模拟生产环境并进行更真实的测试。 它是一个重要的测试环境,需要保持稳定和可靠。通过对修复的bug再次提交到测试环境测试,可以确保预生产环境中的软件版本是经过验证的,并且没有明显的问题。

当然,也不是非要这么做不可,紧急情况下,也可以选择直接发到预生产重新测试,只要你保证你的代码 99% 没问题了。

2. 代码合并错误,并且已经推送到远程分支,如何解决?

假设是在本地合并,本来要把特性分支合并到 uat 分支,结果不小心合到了 release 分支(绝对不是我自己的案例,绝对不是。。。虽然好在最后同事本地有我提交前的版本,事情就简单很多了)

首先切换到特性分支合并到的错误分支,比如是 release

shell复制代码git checkout release

然后查看最近的合并信息(按 q 退出查看)

shell复制代码git log --merges

撤销合并

shell复制代码git revert -m 1 <merge commit ID>
  • 这里的 merge commit ID 就是上一步查询出来的 ID 或者 ID 的前几个字符

最后,撤销远程仓库的推送

shell复制代码git push -f origin release
  • 这个命令会强制推送本地撤销合并后的 release 分支到远程仓库,覆盖掉远程仓库上的内容。(即,得通过一个新的提交来“撤销”上一次的提交,本质上是覆盖)

3. 当前分支有未提交的修改,但是暂时不想提交,想要切换到另一个分支该怎么做?

例如:你正在开发 B 需求,突然产品说 A 需求有点问题,让你赶紧改改,但是当前 B 需求还没开发完成,你又不想留下过多无用的提交记录,此时就可以按照下面这样做:

首先,可以将当前修改暂存起来,以便之后恢复

shell复制代码git stash

然后切换到目标分支,例如需求 A 所在分支

shell复制代码git checkout feat-a-jie

修改完 A 需求后,需要先切换回之前的分支,例如需求 B 所在分支

shell复制代码git checkout feat-b-jie

如果你不确定之前所在的分支名,可以使用以下命令列出暂存的修改以及它们所属的分支:

shell复制代码git stash list

最后从暂存中恢复之前的修改

shell复制代码git stash pop

此时你的工作区就恢复如初了!

4. 扩展

本来是想详细补充这部分的一些重要知识的,但是考虑到如果是小白的话,可能一下子接受不了这么多内容。如果你对以下内容感兴趣的话,也可以单独进行搜索,或者看看之后有没有时间可以单独出几篇讲讲🤤

4.1 cherry-pick 指令

  1. 作用:选择某些提交的变更并将其应用到当前分支
  2. 与 merge 的区别:如果你需要另一个分支的所有代码变动。那么就采用 merge;如果你只需要部分代码变动(某几个提交),那么就采用 cherry-pick
  3. 场景:有时候分支不一定是完全按照需求号来开发的,可能按照周期来进行开发,那当前版本内的分支上,可能就会包含着很多需求的提交,这时候,如果产品要求你只上某一个需求,但是其他的暂时还不能上,那就需要使用到 cherry pick 的操作,将该需求囊括的所有提交应用到对应环境分支上。一定要注意梳理清楚被拆分需求是由多少 commit 组成的,不要有遗漏和多选。
  4. 简单示例:
shell复制代码## 查看最近提交(按 q 退出查看)
git log

## 切换到要应用的分支(比如提测时)
git checkout test

## 将指定的提交应用于当前分支(commitHash 就是通过第一步查询到的),这会在当前分支产生一个新的提交
git cherry-pick <commitHash>
  1. 了解更多可以阅读:阮一峰的 git cherry-pick 教程

4.2 rebase 指令

  1. 作用:对一个分支做「变基」操作。将当前分支的提交挪动到另一个分支的最新提交之后,这样可以创建一个更线性、清晰的提交历史
  2. 与 merge 的区别:merge 会保留合并分支的提交历史,而 rebase 会改写提交历史
  3. 场景1:合并多次提交纪录。例如之前提到的:“当前分支有未提交的修改,但是暂时不想提交,想要切换到另一个分支该怎么做?”的另一个解决办法,就是先提交了,之后再把这些提交记录合并成一个提交记录。
  4. 场景2:合并分支。
  5. 切记:永远不要 rebase 一个共享分支

注意:rebase 和 cherry-pick 使用不当都会给团队协作带来一些不可预估的问题,请尽量在了解清楚后再进行使用。

4.3 三种广泛使用的 Git 工作流程规范

  1. Git flow
  2. Github flow
  3. Gitlab flow(本文类似于该工作流程)

具体可以阅读:阮一峰的 Git 工作流程

在团队内部使用规范的 Git 工作流程,可以帮助我们提高协作效率,减少混乱和冲突。比如规定好分支结构和命名规范,将使得代码库的分支结构更加清晰明了,更易于管理。反之,开发者可能会按照自己的喜好随意创建分支,导致代码库分支结构混乱。

除此之外还有很多好处,降低错误风险、增加代码可追溯性、简化发布流程、促进持续集成与持续交付等等。

五、写在最后

Git 从发布至今,已经发展了近 20 年,在这期间衍生了成百上千种有关 Git 的管理工具和协同规范,不同公司甚至是公司内部不同集团不同部门使用的规范都可能不尽一致,本文只分享本人在工作过程中真实使用到的开发工作流程,并且个人认为以上内容是具有一定普适性的,能够帮助到新人或者小白的一些基础知识。最后,遵循团队项目规范才能真正提高团队的协作效率。

作者:JIE
链接:https://juejin.cn/post/7327863960008392738
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

SQLite 3.22.0 -3.26.0 2018 年新增加的 SQL 功能

SQLite 是一个被大家低估的数据库,但有些人认为它是一个不适合生产环境使用的玩具数据库。事实上,SQLite 是一个非常可靠的数据库,它可以处理 TB 级的数据,但它没有网络层。接下来,本文将与大家共同探讨 SQLite 在过去一年中最新的 SQL 功能。

SQLite “只是”一个库,它不是传统意义上的服务器。因此,在某些场合下,它确实不合适。但是,在相当多的其他场合,它却是最合适的选择。SQLite 号称是部署和使用最广泛的数据库引擎。我认为这很有可能,因为 SQLite 没有版权的限制。无论何时,只要开发者想使用 SQL 在文件中存储结构化的数据,SQLite 应是首选方案。

SQLite 的 SQL 方言也非常强大。它比 MySQL 早四年就开始支持 with 语句。最近,它还实现了对于窗口函数的支持,这仅仅比 MySQL 晚五个月。

接下来,本文将介绍 SQLite 在 2018 年新增加的 SQL 功能,也就是 SQLite 从版本 3.22.0 到 3.26.0 所新增加的 SQL 功能。

具体内容包括:

  1. 布尔字面量和判断

  2. 窗口函数

  3. Filter子句

  4. Insert … on conflict (“Upsert”)

  5. 重命名列

  6. 在Modern-SQL.com上接下来

布尔变量和判断

SQLite支持“假”布尔值:它接受Boolean作为类型的名称,但它将其当作整数看待(这一点非常类似于MySQL)。真值true和false分别由数值1和0表示(这一点和C语言一样)。

从版本3.23.0开始,SQLite将关键字true和false分别用数字1和0表示,并支持is [not] true | false的判断语句。现在,它不再支持关键字unknown。开发者可以使用空值null来代替,因为unknown和null的布尔值是一样的。

在INSERT和UPDATE语句中,字面量true和false可以大大提高values和set子句的可读性。

is [not] true | false这个判断语句很有用,它与比较操作的含义不一样:

我们来比较一下
WHERE c <> FALSE
WHERE c IS NOT FALSE

在上面的例子中,如果c是null, 那么c <> false的结果是unknown.

这是因为WHERE子句只接受结果为true的值,它会过滤掉结果为false或unknown的值。这样,它就会把对应的行从结果中去掉。

与此相对应,如果c是null,那么,c is not false的判断结果是true。因此,第二个WHERE子句也将包含c是null的行。

要达到同样的效果,您可以采用的另外一种方法是增加单独处理null值的子句。也就是使用语句:
WHERE c <> FALSE   OR c IS NULL

这种形式的语句更长并且有一些冗余语句(c被使用了两次)。长话短说,可以使用is not false判断来替代这个or…is-null的语句。更详细的内容,请参考“Binary Decisions Based on Three-Valued Results”。

SQLite中对布尔字面量和布尔判断的支持现在和其他开源数据库接近,唯一的差距是SQLite不支持is[not] unknown(你可以使用is [not] null来代替)。有趣的是,这些功能在下面提到的商用产品中还不可用。

0:只支持true,false.不支持notknown,如果需要,用null代替

1:不支持is [not] unknown,如果需要,用is [not] null代替

窗口函数

SQLite 3.25.0引入了窗口函数。如果你知道窗口函数,那么也知道这是一件大事。如果你不了解窗口功能,请你自己学习如何使用。这篇文章不会具体解释窗口函数,但请相信:它是最重要的“现代”SQL特性。

SQLite对over子句的支持与其他数据库非常接近。唯一值得注意的限制是range语句不支持数字或间隔距离(仅支持current row和unbounded preceding|following)。在发布sqlite 3.25.0时,SQL Server和PostgreSQL具有同样的限制。PostgreSQL 11消除了这一限制。

0:没有变化

1:Range范围定义不支持datetime类型

2:Range范围不接受关键字 (只支持unbounded和current row)

SQLite对于窗口函数的支持在业界是领先的。它不支持的功能在其他一些主要产品中也同样不支持(在聚合中语句中的distinct,width_bucket, respect|ignore nulls和from first|last等语句)。

0:同样没有ORDER BY 语句

1:不允许负偏移量,nulls的特定处理:lead(, ‘IGNORE NULLS’),这里是字符串参数

2:没有缺省值(第三个参数),不支持respect|ignore nulls语句

3:不允许负偏移量,不支持ignore nulls语句

4:不允许负偏移量

5:不支持respect|ignore nulls语句

6:不允许负偏移量,不支持respect|ignore nulls语句

7:nulls的特定处理:first_value(, 1, null, ‘IGNORE NULLS’) ,这里是字符串参数。

8:不支持ignore nulls语句

9:不支持ignore nulls语句和from last语句

过滤语句

虽然filter语句只是语法糖——你也可以很容易地使用表达式来获得相同的结果——我认为它也是必不可少的语法糖,因为它能使人们更加容易地学习和理解SQL语句。

看看下面的select子句,您觉得哪一个更容易理解?

SELECT SUM(revenue) total_revenue     , SUM(CASE WHEN product = 1                 THEN revenue            END          ) prod1_revenue   ...
SELECT SUM(revenue) total_revenue     , SUM(revenue) FILTER(WHERE product = 1) prod1_revenue   ...

此示例很好地总结了filter子句的作用:它是聚合函数的后缀,可以在进行聚合之前根据特定条件,过滤掉相应的行。pivot技术是filter子句最常见的用例。这包括将实体属性值(EAV)模型中的属性转换为表格的列,如果想了解更多的内容,可以参考链接“filter-Selective Aggregates”(https://modern-sql.com/feature/filter)。

SQLite 从版本3.25.0开始,在使用over子句的聚合函数中支持了filter子句,但是在使用group by子句的聚合函数中还不支持。不幸的是,这意味着您仍然无法在SQLite中使用filter语句来处理上述情况。你必须像以前一样使用case表达式。我真的希望SQLite在这一点上能尽快做到。

Insert … on conflict (“Upsert”)

SQLite 从版本3.24.0开始,引入了“upsert”概念:它是一个insert语句,可以优雅地处理主键和唯一约束的冲突。您可以选择忽略这些冲突(在on conflict语句中什么都不做)或者更新当前行(在on conflict语句中执行更新操作)。

这是一个特有的SQL扩展,即它不是标准SQL的一部分,因此在下面的矩阵中是灰色的。但是,SQLite遵守与PostgreSQL相同的语法来实现此功能0。该标准提供了对merge语句的支持。

与PostgreSQL不同,SQLite在以下语句中存在问题。
INSERT INTO targetSELECT *  FROM source    ON CONFLICT (id)    DO UPDATE SET val = excluded.val
根据说明文档,这是因为解析器无法判断关键字ON是SELECT语句的连接约束还是upsert子句的开头。你可以通过向查询中添加子句来解决,例如where true。
INSERT INTO targetSELECT *  FROM source WHERE true    ON CONFLICT (id)    DO UPDATE SET val = excluded.val

0:同样记录insert、update、delete和merge操作的错误信息 (“DML error logging”)

1:On conflict语句不能紧挨查询的from语句,如果需要,可以添加  where true语句来分隔。

重命名列

SQLite引入的另一个特有功能是重命名基准数据库表中的列1。标准的SQL不支持此类功能2。

SQLite遵循其他产品常用的语法来重命名列:

ALTER TABLE … RENAME COLUMN … TO

0:请查阅 sp_rename.

其他消息

在2018年,SQLite除了在SQL语法上的变化,还有一些应用程序接口(API)的变化。你可以查阅sqlite.com(https://www.sqlite.org/news.html)上的新闻部分来了解更详细的消息。

脚标:

  • 0:SQLite通常遵循PostgreSQL语法,Richard Hipp将此称为PostgreSQL会怎么做(WWPD)。

  • 1:基准数据库表是指用Create table语句创建的数据库表。派生的数据库表(如Select语句返回的查询结果集)中的列名可以通过SELECT语句、FROM语句或WITH语句来进行改变

  • 2:据我所知,也许可以通过可更新视图或派生的列来模拟该功能。 

文章来源:网络

转自:

裁员、清货、关门,盒马到底怎么了?

最近,各地盒马撤柜关门的消息,源源不断地冒出来。
有网友发现,自己买的盒马商品没人送货。送来的东西日期不新鲜,种类也变少了,爱喝的苹果汁下架了,常买的鸡肉也断货了。
就连我们办公室很多同事,也发现买到临期产品的概率变高了。
他还表示,这在曾经追求高质量的盒马,是绝不会发生的。
当有人贴出盒马货架被清空,既没人补货,店里也没有顾客的照片时。
差评君意识到这事有点不对头了啊。
可就在 3 个月前,盒马 CEO 侯毅还放出了豪言壮语,未来盒马的使命是 10 年 1 万亿和网友、同事的所见所闻一对比,这矛盾和冲突感,妥妥得拉满了。
差评君也大概了解了一下,去年 5 月的时候,盒马憋住一口气要上市,可惜最后没憋住,从那之后,似乎就在不断漏气了。
甚至于,一度传出了阿里要出售盒马的传闻。
虽然官方立马跳出来辟谣了,说没有的事。
但稍微留意一下,确实能发现盒马这日子是不太好过。
开头网友传的盒马关门,确实是存在的, 2 月底大连盒马门店就宣布停业。不过,盒马一向开关门速度飞快,效益不好了就直接砍掉。
根据知危编辑部采访到的武汉员工的说法, 3 月份,武汉还要再关 3 家。
不光是对亏钱的门店下手,在日常生活中,盒马也是能省则省。
 在一些帖子的下方,就有员工吐槽过盒马近乎离谱的省钱法子。
比如封厕所、不开门就不开灯,一关门立马关灯、一个人顶十个人用。巴不得一张厕纸,撕成两份使。
这省钱的算盘,还打到了员工的工资和合同上。
2 月初的时候,合肥等城市的门店,就只招收小时工或者兼职。像合肥后场拣货工,每件从 0.26 元,降到了 0.17 元。全国各地降薪幅度都在 20% 左右。
同时,盒马试图把正式员工转成外包三方员工
知危编辑部采访的武汉门店员工就告诉咱们,他在 2 月 18 号就收到了转临时工的通知。
取消五险一金、节假日三倍薪资、年假、十三薪,转为三方合同。要是不同意,就自己拍屁股滚蛋( 没有 N+1 )
 “ 有员工同意转了,还有哭着签字的。 ” 
受访者估算了一下,按一家店 20 个员工算,一年能给盒马省 40 万元。
这边疯狂地压榨员工,在消费者那边,盒马也是各种抠抠搜搜。
 以前免费提供的包装袋,现在每单强制收 1 元。在北京、南京、长沙的盒马,免送费门槛还升到了 99 元。
对于很多习惯在盒马买菜做饭的人来说, 99 元的凑单门槛能要了他们的命。。
大家实在想不通了,怎么大手大脚的阿里富哥儿,兜里好像突然没钱了。
如果我们看数据,会发现虽然盒马整体仍在亏损,但前年亏损已经在变少了。侯毅还在员工全员信中透露,其中盒马鲜生单个业务,前年甚至已经盈利。
按理来说,日子应该要慢慢好起来了,结果去年阿里一拆分,盒马独立了出来。
离开了大家庭之后,盒马的日子也开始紧巴起来了,如何盈利,成了当务之急。
所以,一整年时间,盒马都在做各种尝试。
 在 9 月份前,盒马想到的赚钱法子是把炮口对准山姆,去抢中产的钱。
先对标山姆推出了每年 658 元的钻石会员。还搞了一场轰轰烈烈的移山运动,正面比价、比货,打出半折的优惠。
最离谱的是跑到山姆门口,一车一车地往盒马运人。。。
虽然没伤到山姆分毫,但这招棋也算给盒马带来了不少好处。根据瑞泽洞察统计的数据,移山期间,盒马 APP 周均日活涨了 13.3% ,山姆也小涨了 3.9% 。
很多人以为,和山姆的这场贴身肉搏已经够猛的了。
结果没想到,移山结束才一个月,盒马又上新一系列大开大合的操作。
盒马干脆半放弃了自己的偏高价,重品质定位,打算用极致的低价硬刚到底。
去年 10 月,整了个全面 “ 折扣化 ” ,全场 5000 多款商品降价甩卖,之后连成本极高的生鲜价格都被打下来了。
随便举两个例子,以前卖 79 元的草莓盒子,现在只要 59 元、 39.9 的肥牛片降到了 31.9 。
面对一直居高不下的配送成本,盒马也选择重拳出击。
一位接近盒马的人士对知危表示,线上订单如果没到 99 元,对盒马来说送一单亏一单
 不巧的是,很多店铺的线上订单都能到 80 % 以上,有配送员算过,平均每单配送费都在六七元左右。每天平均 3000 单,一个盒马门店一个月就要支出五六十万。
估计是想止一波配送费的血,部分城市的盒马,便强制把运费门槛提高到了 99 元。
 但,这也让习惯了低运费门槛的消费者们,挨了一闷棍。
而且,除了提高免运费门槛,盒马还额外加设了线下特惠价,逼用户到店消费。甚至还有过线下价格,比线上会员价还便宜的情况,这也导致很多会员抱怨盒马不地道。
结果,盒马压根懒得安抚人心,直接挥刀把每年创收五亿多的会员制度全部砍了。
只能说,没人能阻止它,给来线下的家人们谋低价。
可能看到这,你会觉得有点奇怪。盒马不是说要赚钱吗?1 块钱的包装费都要抠搜,产品降价几十块,还有赚头吗?
因为盒马的砍一刀,不只砍在自己身上,也砍了背后的供应商们。
产品方面的降价,主要来自两块地方:一是自有品牌,二是供应商。
在自有品牌方面,盒马决定把占比在3 年内提高到 70% 。所以,我们稍微留心一点,就能发现货柜里的东西,不知道啥时候都印上盒马俩字了。
因为卖自有品牌很爽,利润很高,没有品牌商赚差价。卖多少钱,完全自己说了算。
除此之外,对那些供应商们,盒马也是下了狠手。
先是精简 SKU ( 库存量单位 ),下架 3000 多个产品。
另一方面,对供应商压价。根据茶叶品牌 Chabiubiu 创始人的爆料贴,盒马要他们降一半的价格。
侯毅公开地和供应商们说过: “ 希望你们一次性给我最低价。 ” 
只要愿意给低价,产品有竞争力,那就合作。如果不肯给,再大的牌子也不伺候。如此高压下,导致去年年末就有大量的品牌跳出来说,要跟盒马 “ 断交 ” 。
也难怪侯毅说,有人要封杀他们。如果我是供应商,已经想蹲他楼下,等着暴揍他了。
盒马也知道这样会得罪不少品牌,但没办法,拆分之后,盒马每一步,走的压力都很大。
去年 2 月,蔡崇信在阿里巴巴业绩会上说: “ 我们的资产负债表上依然有一些传统的实体零售业务,这些也不是我们的核心聚焦。如果能够完成退出的话,也是非常合理的。 ” 
 面对不断调整的阿里,盒马必须尽快完成它所谓的 “ 753  ” 大业即 KA ( 大客户 )商品是市场价的七折,自有品牌是市场价的五折,临期商品是市场价的三折
归根结底一句话,用便宜的价格来抢用户,扩规模。
但价格压得越低,那些供货稳定的大牌子,就更懒得陪盒马玩。全靠自营,现在的盒马能力又还不够格。
好在盒马现在在圈子里,还有点号召力。
曾经公开断供的王小卤,转头又和盒马牵上手了。盒马还顺便谈下了百世、玛氏。。。

也不可否认,盒马也依然拥有着一批相当忠实的用户,对很多人来说,盒马是更适合中国人体质的“ 山姆 ”。

他们烧钱烧出来的日鲜蔬菜、肉类、品质在线的甜点,以及快速的配送网络,对于大城市里不好去菜市场的人来说,确实是很方便。

反正差评君还记得,公司附近的盒马刚开业的时候,编辑部不少同事可都是翘班去实地考察的。用同事的原话来形容,第一次去盒马,甚至有种“ 楚雨荨第一次去美特斯邦威 ”的即视感。

当然玩笑归玩笑,盒马确实是不少人对新零售生鲜超市这个概念的启蒙。

但赔钱的生意终是做不长久,只能看盒马接下来,还会祭出什么样的牌吧。

撰文:四大  编辑:江江 & 面线 & 大饼  封面:焕妍

图片、资料来源

知危:盒马勒紧裤腰带:员工被转为临时工,有人含泪签协议、被暂缓IPO上市,但盒马不得不狂飙

chabiubiu:一位女性新消费创业者的艰难求生

第一财经“”盒马为什么得罪会员也要线上线下不同价

市界:盒马连砍了自己三刀

财新:盒马推折扣化变革 CEO侯毅决心告别KA模式

转自: https://mp.weixin.qq.com/s/u99dMf-odSpLqLH86zyOSw

6月15日—17日!2024年上海中考时间公布

2月29日,上海市教委公布《2024年本市高中阶段学校招生工作的若干意见》(以下简称《若干意见》),今年上海中考时间为6月15日-17日。


2024年上海市初中学业水平考试

时间安排


根据《若干意见》,本市高中阶段学校招生以初中学业水平考试(以下简称“学业考试”)语文、数学、外语、道德与法治、历史、体育与健身和综合测试(含物理、化学、跨学科案例分析、物理和化学实验操作)的成绩作为录取的基本依据,总分750分。


2024年学业考试全部安排在标准化考点进行,按照国家教育考试的标准和要求规范考务工作组织实施。回户籍地或居住证登记地址所在区参加中招报名(以下简称“跨区报名”)的学生,在学籍所在区参加学业考试。往届生、返沪生在报名所在区参加学业考试。残疾学生参加考试可申请相应合理便利。学业考试的语文、数学、英语、道德与法治和历史科目以及综合测试笔试实行全市统一网上评卷;其他科目考试评定工作由各区教育局组织实施。


2024年本市中招志愿填报统一在学业考试后、成绩发布前进行。各批次志愿在市级统一平台进行网上填报。学生须在规定时间内登录“上海招考热线”网站进行志愿填报。填报完成后,学生本人及家长(监护人)须进行志愿书面确认。


根据《实施意见》,今年上海高中阶段学校招生录取(以下简称“中招录取”)分三个批次进行,依次为自主招生录取、名额分配综合评价录取、统一招生录取。其中,自主招生录取在学业考试后进行,录取顺序依次为:市实验性示范性高中、市特色普通高中自主招生录取;市级优秀体育学生、艺术骨干学生自主招生录取;国际课程班和中外合作办学高中自主招生录取;中职校自主招生录取。名额分配综合评价录取工作在自主招生录取后进行。名额分配综合评价录取的总分由学业考试总成绩和综合素质评价成绩两部分构成,录取顺序依次为:“名额分配到区”录取;“名额分配到校”录取。统一招生录取工作中,“1至15志愿”招生录取由各区招考机构负责。各区招考机构根据学生学业考试总成绩和志愿,按平行志愿方式,从高分到低分进行录取。“1至15志愿”未被录取的学生,由各区招考机构负责进行征求志愿的填报和录取工作。


市教委要求,市、区招考机构以及各招生学校必须按《若干意见》中“2024年上海市高中阶段学校招生公布公示内容” 适时对外公布有关信息。各区招考机构网站要建立健全中招专栏,加强招生工作信息公开力度。


2024年上海市高中阶段学

招生日程表


文字:符佳

编辑:储咏瑜

* 转载请注明来自浦东发布官方微信


*直击引领区丨将人工智能嵌入全线业务,ABB发布新一轮AI战略

*守好船载危险货物审核关,浦东海事局全力维护辖区水上安全

转自: 浦东发布

从 Flask 切到 FastAPI 后,起飞了!

本文翻译自 Moving from Flask to FastAPI, 作者:Amal Shaji

刚好笔者这几天上手体验 FastAPI,感受到这个框架易用和方便。之前也使用过 Python 中的 Django 和 Flask 作为项目的框架。Django 说实话上手也方便,但是学习起来有点重量级框架的感觉,FastAPI 带给我的直观体验还是很轻便的,本文翻译的这篇文章就会着重介绍 FastAPI 和 Flask 的区别。

Python 是最流行的编程语言之一。从脚本到 API 开发再到机器学习,Python 都有着它自己的足迹。因为 Python 注重开发者的体验和其所能提供的大量工具而大受欢迎。网络框架 Flask 就是这样一个工具,它在机器学习社区中很受欢迎。它也被广泛用于 API开发。但是有一个新的框架正在崛起: FastAPI。与 Flask 不同,FastAPI 是一个 ASGI(Asynchronous Server Gateway Interface 异步服务器网关接口)框架。与 Go 和 NodeJS 一样,FastAPI 是最快的基于 Python 的 Web 框架之一。

本文针对那些有兴趣从 Flask 转移到 FastAPI 的人,比较和对比了 Flask 和 FastAPI 的常见模式。

# FastAPI vs Flask

FastAPI 的构建考虑了以下三个主要问题:

  • 速度
  • 开发者经验
  • 开放标准

你可以把 FastAPI 看作是把 Starlette、Pydantic、OpenAPI 和 JSON Schema 粘合在一起的胶水。

  • 本质上说,FastAPI 使用 Pydantic 进行数据验证,并使用 Starlette 作为工具,使其与 Flask 相比快得惊人,具有与 Node 或 Go 中的高速 Web APIs 相同的性能。
  • Starlette + Uvicorn 提供异步请求能力,这是 Flask 所缺乏的。
  • 有了 Pydantic 以及类型提示,你就可以得到一个具有自动完成功能的良好的编辑体验。你还可以得到数据验证、序列化和反序列化(用于构建一个 API),以及自动化文档(通过 JSON Schema 和 OpenAPI )。

也就是说,Flask 的使用更为广泛,所以它经过了实战检验,并且有更大的社区支持它。由于这两个框架都是用来扩展的,Flask 显然是赢家,因为它有庞大的插件生态系统。

建议:

  • 如果你对上述三个问题有共鸣,厌倦了 Flask 扩展时的大量选择,希望利用异步请求,或者只是想建立一个 RESTful API,请使用 FastAPI。
  • 如果你对 FastAPI 的成熟度不满意,需要用服务器端模板构建一个全栈应用,或者离不开一些社区维护的 Flask 扩展,就可以使用 Flask。

# 开始

 安装

与任何其他 Python 包一样,安装非常简单。

Flask

pip install flask

# or
poetry add flask
pipenv install flask
conda install flask

FastAPI

pip install fastapi uvicorn

# or
poetry add fastapi uvicorn
pipenv install fastapi uvicorn
conda install fastapi uvicorn -c conda-forge

与 Flask 不同,FastAPI 没有内置的开发服务器,因此需要像 Uvicorn 或 Daphne 这样的 ASGI 服务器。

 “Hello World” 应用

Flask

# flask_code.py

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return {"Hello""World"}

if __name__ == "__main__":
    app.run()

FastAPI

# fastapi_code.py

import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def home():
    return {"Hello""World"}

if __name__ == "__main__":
    uvicorn.run("fastapi_code:app")

reload=True 这样的参数可以被传递到 uvicorn.run() 中,以实现开发时的热重载。

或者,您可以直接从终端启动服务器:

uvicorn run fastapi_code:app

热加载模式:

uvicorn run fastapi_code:app --reload

# 配置

Flask 和 FastAPI 都提供了许多选项来处理不同环境的不同配置。两者都支持以下模式:

  1. 环境变量
  2. 配置文件
  3. 实例文件夹
  4. 类和继承

有关更多信息,请参阅其各自的文档:

  • Flask: https://flask.palletsprojects.com/en/2.0.x/config/
  • FastAPI:https://fastapi.tiangolo.com/advanced/settings/

Flask

import os
from flask import Flask

class Config(object):
    MESSAGE = os.environ.get("MESSAGE")

app = Flask(__name__)
app.config.from_object(Config)

@app.route("/settings")
def get_settings():
    return { "message": app.config["MESSAGE"] }

if __name__ == "__main__":
    app.run()

现在,在你运行服务器之前,设置适当的环境变量:

export MESSAGE="hello, world"

FastAPI

import uvicorn
from fastapi import FastAPI
from pydantic import BaseSettings

class Settings(BaseSettings):
    message: str

settings = Settings()
app = FastAPI()

@app.get("/settings")
def get_settings():
    return { "message": settings.message }

if __name__ == "__main__":
    uvicorn.run("fastapi_code:app")

同样,在运行服务器之前,设置适当的环境变量:

export MESSAGE="hello, world"

# 路由, 模板和视图

 HTTP 方法

Flask

from flask import request

@app.route("/", methods=["GET", "POST"])
def home():
    # handle POST
    if request.method == "POST":
        return {"Hello""POST"}
    # handle GET
    return {"Hello""GET"}

FastAPI

@app.get("/")
def home():
    return {"Hello""GET"}

@app.post("/")
def home_post():
    return {"Hello""POST"}

FastAPI 为每个方法提供单独的装饰器:

@app.get("/")
@app.post("/")
@app.delete("/")
@app.patch("/")

 URL 参数

通过 URL(如 /employee/1 )传递信息以管理状态:

Flask

@app.route("/employee/<int:id>")
def home():
    return {"id": id}

FastAPI

@app.get("/employee/{id}")
def home(id: int):
    return {"id": id}

URL参数的指定类似于一个 f-string 表达式。此外,你还可以利用类型提示。这里,我们在运行时告诉 Pydantic, idint 类型的。在开发中,这也可以帮助完成更好的代码完成度。

 查询参数

与 URL 参数一样,查询参数(如 /employee?department=sales )也可用于管理状态(通常用于过滤或排序):

Flask

from flask import request

@app.route("/employee")
def home():
    department = request.args.get("department")
    return {"department": department}

FastAPI

@app.get("/employee")
def home(department: str):
    return {"department": department}

 模板

Flask

from flask import render_template

@app.route("/")
def home():
    return render_template("index.html")

默认情况下,Flask会在 “templates “文件夹中寻找模板。

FastAPI

你需要安装 Jinja:

pip install jinja2

实现:

from fastapi import Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse

app = FastAPI()

templates = Jinja2Templates(directory="templates")

@app.get("/", response_class=HTMLResponse)
def home(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

对于 FastAPI,你需要明确地定义 “模板 “文件夹。然后对于每个响应,需要提供请求上下文。

 静态文件

Flask

默认情况下,Flask 从“static”文件夹中提供静态文件。

FastAPI

在 FastAPI 中,需要为静态文件挂载一个文件夹:

from fastapi.staticfiles import StaticFiles

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")

 异步任务

Flask

从 Flask 2.0 开始,您可以使用 async/await 创建异步路由处理程序:

@app.route("/")
async def home():
    result = await some_async_task()
    return result

有关 Flask 中异步视图的更多信息,请查看 Flask 2.0 中的异步一文。

Flask 中的异步也可以通过使用线程(并发)或多处理(并行)或 Celery 或 RQ 等工具来实现:

  1. Asynchronous Tasks with Flask and Celery:https://testdriven.io/blog/flask-and-celery/
  2. Asynchronous Tasks with Flask and Redis Queue:https://testdriven.io/blog/asynchronous-tasks-with-flask-and-redis-queue/

FastAPI

由于 FastAPI 对 asyncio 的原生支持,它极大地简化了异步任务。要使用的话,只需在视图函数中添加 async 关键字:

@app.get("/")
async def home():
    result = await some_async_task()
    return result

FastAPI 还具有后台任务功能,您可以使用它来定义返回响应后要运行的后台任务。这对于不需要在发送回响应之前完成的操作很有用。

from fastapi import BackgroundTasks

def process_file(filename: str):
    # process file :: takes minimum 3 secs (just an example)
    pass

@app.post("/upload/{filename}")
async def upload_and_process(filename: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(process_file, filename)
    return {"message""processing file"}

在这里,响应将被即时发送,而不会让用户等待文件处理完成。

当你需要进行繁重的后台计算时,或者你需要一个任务队列来管理任务(tasks)和工作者(workers)时,你可能想使用Celery 而不是 BackgroundTasks。更多内容请参考 FastAPI 和 Celery 的异步任务:https://testdriven.io/blog/fastapi-and-celery/

 依赖注入

Flask

虽然你可以实现自己的依赖注入解决方案,但 Flask 默认没有真正的一流支持。相反,你需要使用一个外部包,如 flask-injector。

FastAPI

另一方面,FastAPI 具有处理依赖注入的强大解决方案。

例如:

from databases import Database
from fastapi import Depends
from starlette.requests import Request

from db_helpers import get_all_data
def get_db(request: Request):
    return request.app.state._db

@app.get("/data")
def get_data(db: Database = Depends(get_db)):
    return get_all_data(db)

因此,get_db 将获取对在应用程序的启动事件处理程序中创建的数据库连接的引用。 Depends 然后用于向 FastAPI 指示路由“依赖于” get_db。因此,它应该在路由处理程序中的代码之前执行,并且结果应该“注入”到路由本身。

 数据校验

Flask

Flask 没有任何内部数据验证支持。您可以使用功能强大的 Pydantic 包通过 Flask-Pydantic 进行数据验证。

FastAPI

FastAPI 如此强大的原因之一是它支持 Pydantic。

from pydantic import BaseModel

app = FastAPI()

class Request(BaseModel):
    username: str
    password: str

@app.post("/login")
async def login(req: Request):
    if req.username == "testdriven.io" and req.password == "testdriven.io":
        return {"message""success"}
    return {"message""Authentication Failed"}

在这里,我们接受一个模型 Request 的输入。该 payload 必须包含一个用户名和密码。

# correct payload format
✗ curl -X POST 'localhost:8000/login' 
    --header 'Content-Type: application/json' 
    --data-raw '{"username": "testdriven.io","password":"testdriven.io"}'

{"message":"success"}

# incorrect payload format
✗ curl -X POST 'localhost:8000/login' 
    --header 'Content-Type: application/json' 
    --data-raw '{"username": "testdriven.io","passwords":"testdriven.io"}'

{"detail":[{"loc":["body","password"],"msg":"field required","type":"value_error.missing"}]}

注意到这个请求。我们把密码 passwords 作为一个键而不是 password 传递进去。Pydantic 模型会自动告诉用户,password 字段是缺失的。

 序列化和反序列化

Flask

最简单的序列化方法是使用 jsonify:

from flask import jsonify
from data import get_data_as_dict

@app.route("/")
def send_data():
    return jsonify(get_data_as_dict)

对于复杂的对象,Flask 开发者经常使用 Flask-Marshmallow

FastAPI

FastAPI 自动序列化任何返回的字典 dict 。对于更复杂和结构化的数据,使用 Pydantic:

from pydantic import BaseModel

app = FastAPI()

class Request(BaseModel):
    username: str
    email: str
    password: str

class Response(BaseModel):
    username: str
    email: str

@app.post("/login", response_model=Response)
async def login(req: Request):
    if req.username == "testdriven.io" and req.password == "testdriven.io":
        return req
    return {"message""Authentication Failed"}

在这里,我们添加了一个包含三个输入的 Request 模型:用户名、电子邮件和密码。我们还定义了一个仅包含用户名和电子邮件的 Response 模型。输入 Request 模型处理反序列化,而输出 Response 模型处理对象序列化。然后通过 response_model 参数将响应模型传递给装饰器。

现在,如果我们将请求本身作为响应返回,Pydantic 将省略 password ,因为我们定义的响应模型不包含密码字段。

例如:

# output
✗ curl -X POST 'localhost:8000/login' 
    --header 'Content-Type: application/json' 
    --data-raw '{"username":"testdriven.io","email":"admin@testdriven.io","password":"testdriven.io"}'

{"username":"testdriven.io","email":"admin@testdriven.io"}

 中间件

中间件被用来在每个请求被视图功能处理之前应用逻辑。

Flask

class middleware:
    def __init__(self, app) -> None:
        self.app = app

    def __call__(self, environ, start_response):
        start = time.time()
        response = self.app(environ, start_response)
        end = time.time() - start
        print(f"request processed in {end} s")
        return response

app = Flask(__name__)
app.wsgi_app = middleware(app.wsgi_app)

FastAPI

from fastapi import Request

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    print(f"request processed in {process_time} s")
    return response

@app.middleware("http") 装饰器是在 FastAPI 中创建中间件的必备工具。上述中间件计算处理请求所花费的时间。视图函数处理请求后,计算总处理时间并将其作为响应头返回。

# flask output(logs)
request processed in 0.0010077953338623047 s
127.0.0.1 - - [22/Sep/2020 18:56:21"GET / HTTP/1.1" 200 -

# fastapi output(logs)
request processed in 0.0009925365447998047 s
INFO:     127.0.0.1:51123 - "GET / HTTP/1.1" 200 OK

 模块化

随着应用程序的发展,在某些时候你会想把类似的视图、模板、静态文件和模型组合在一起,以帮助把应用程序分解成更小的组件。

Flask

在 Flask 中,蓝图被用来实现模块化:

# blueprints/product/views.py
from flask import Blueprint

product = Blueprint("product", __name__)

@product.route("/product1")
    ...
# main.py

from blueprints.product.views import product

app.register_blueprint(product)

FastAPI

同时,在 FastAPI 中,模块化是通过 APIRouter 实现的:

# routers/product/views.py
from fastapi import APIRouter

product = APIRouter()

@product.get("/product1")
    ...
# main.py

from routers.product.views import product

app.include_router(product)

# 其他特点

 自动文档

Flask

Flask 不会自动创建开箱即用的 API 文档。然而,有几个扩展可以处理这个问题,比如 flask-swagger 和 Flask RESTX,但它们需要额外的设置。

FastAPI

默认情况下,FastAPI 支持 OpenAPI 以及 Swagger UI 和 ReDoc。这意味着每个端点都自动从与端点关联的元数据中记录下来。

所有注册的端点都列在这里

此处列出了所有已注册的端点

替代文档

 管理应用

Flask

Flask 有一个广泛使用的第三方管理包,称为 Flask-Admin,用于快速对您的模型执行 CRUD 操作。

FastAPI

截至目前,有两个流行的 FastAPI 扩展用于此:

  1. FastAPI Admin – 功能性管理面板,提供用于对数据执行 CRUD 操作的用户界面。
  2. SQLAlchemy Admin -FastAPI/Starlette 的管理面板,可与 SQLAlchemy 模型一起使用。

 身份认证

Flask

虽然 Flask 没有原生解决方案,但可以使用多个第三方扩展。

FastAPI

FastAPI 通过 fastapi.security 包原生支持许多安全和身份验证工具。通过几行代码,您可以将基本的 HTTP 身份验证添加到您的应用程序中:

import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
    correct_username = secrets.compare_digest(credentials.username, "stanleyjobson")
    correct_password = secrets.compare_digest(credentials.password, "swordfish")
    if not (correct_username and correct_password):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    return credentials.username


@app.get("/whoami")
def who_ami_i(username: str = Depends(get_current_username)):
    return {"username": username}

FastAPI 通过 OpenAPI 标准实现 OAuth2 和 OpenID Connect。

查看官方文档中的以下资源以获取更多信息:

  1. Security Intro:https://fastapi.tiangolo.com/tutorial/security/
  2. Advanced Security:https://fastapi.tiangolo.com/advanced/security/

其他资源

  1. Web Authentication Methods Compared:https://testdriven.io/blog/web-authentication-methods/
  2. Adding Social Authentication to Flask:https://testdriven.io/blog/flask-social-auth/
  3. Session-based Auth with Flask for Single Page Apps:https://testdriven.io/blog/flask-spa-auth/
  4. Securing FastAPI with JWT Token-based Authentication:https://testdriven.io/blog/fastapi-jwt-auth/

 CORS

CORS(跨源资源共享)中间件检查请求是否来自允许的来源。如果是,则将请求传递给下一个中间件或视图函数。如果不是,它会拒绝请求,并将错误响应发送回调用者。

Flask Flask 需要一个名为 Flask-CORS 的外部包来支持 CORS:

pip install flask-cors

基本实现:

from flask_cors import CORS

app = Flask(__name__)

CORS(app)

FastAPI

FastAPI 原生支持 CORS:

from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = ["*"]

app.add_middleware(CORSMiddleware, allow_origins=origins)

# 测试

Flask

import pytest
from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return {"message""OK"}

def test_hello():
    res = app.test_client().get("/")

    assert res.status_code == 200
    assert res.data == b'{"message":"OK"}n'

FastAPI

from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()

@app.get("/")
async def home():
    return {"message""OK"}

client = TestClient(app)

def test_home():
    res = client.get("/")

    assert res.status_code == 200
    assert res.json() == {"message""OK"}

FastAPI 提供了一个 TestClient。有了它,你可以直接用 FastAPI 运行 pytest。有关更多信息,请查看官方文档中的测试指南。

# 部署

 生产服务器

Flask

Flask 默认运行开发 WSGI(Web 服务器网关接口)应用程序服务器。对于生产环境,您需要使用生产级 WSGI 应用服务器,例如 Gunicorn、uWSGI 或 mod_wsgi

安装 Gunicorn:

pip install gunicorn

启动服务:

# main.py
# app = Flask(__name__)

gunicorn main:app

FastAPI

由于 FastAPI 没有开发服务器,您将使用 Uvicorn(或 Daphne)进行开发和生产。

安装 Uvicorn:

pip install uvicorn

启动服务:

python
# main.py
# app = FastAPI()

uvicorn main:app

您可能希望使用 Gunicorn 来管理 Uvicorn,以便同时利用并发性(通过 Uvicorn)和并行性(通过 Gunicorn worker):

# main.py
# app = FastAPI()

gunicorn -w 3 -k uvicorn.workers.UvicornWorker main:app

 Docker

Flask

FROM python3.10-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["gunicorn""main:app"]

这是 Flask 最简单的 Dockerfile 之一。要了解如何针对生产对其进行全面配置,请查看使用 Postgres、Gunicorn 和 Nginx 教程对 Flask 进行 Docker 化。

FastAPI

sql
FROM python3.10-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn""main:app"]

同样,这是一个非常简单的配置。 FastAPI 作者提供了几个生产就绪的 Dockerfile。有关更多信息,请查看官方 FastAPI 文档以及 Dockerizing FastAPI with Postgres、Uvicorn 和 Traefik 教程。

# 总结

退一步讲,Django 和 Flask 是两个最流行的基于 Python 的网络框架(FastAPI 是第三大流行框架)。不过它们(Django 和 Flask)的理念非常不同。Flask 比 Django 的优势在于 Flask 是一个微框架。程序结构由程序员自己决定,不强制执行。开发者可以在他们认为合适的时候添加第三方扩展来改进他们的代码。也就是说,通常情况下,随着代码库的增长,需要一些几乎所有网络应用都需要的通用功能。这些功能与框架的紧密结合,使得终端开发者需要自己创建和维护的代码大大减少。

本文中的代码实例也表达了同样的意思。换句话说,FastAPI 包括许多必要的功能。它还遵循严格的标准,使你的代码可以生产并更容易维护。FastAPI 的文档也非常完善。

虽然 FastAPI 可能不像 Flask 那样久经考验,但越来越多的开发人员正在转向它来提供机器学习模型或开发 RESTful API。切换到 FastAPI 是一个不错的选择。

# 官方文档

  • FastAPI:https://fastapi.tiangolo.com/
  • Flask:https://flask.palletsprojects.com/en/2.0.x/

# 其他资源

  • Porting Flask to FastAPI for ML Model Serving:https://www.pluralsight.com/tech-blog/porting-flask-to-fastapi-for-ml-model-serving/
  • Why we switched from Flask to FastAPI for production machine learning:https://towardsdatascience.com/why-we-switched-from-flask-to-fastapi-for-production-machine-learning-765aab9b3679
  • Awesome Flask:https://github.com/mjhea0/awesome-flask
  • Awesome FastAPI:https://github.com/mjhea0/awesome-fastapi

作者:宇宙之一粟
链接:https://juejin.cn/post/7219225476706140218