🧪 第2章 建立数据库应用程序
📄 一句话概括

这一章的目标是一个「上传图片、跑侦测、把结果存下来」的应用,界面一共三块。 三块拆成三个蓝图,每个蓝图自带路由、模板、静态文件,谁都不认识谁。 地址前缀在注册时才给,所以同一套页面能挂到两个地方,端点名自动带蓝图名。

📝 本章和前面几章的关系

第 1 章的应用全写在单个文件里。教材从这一章起换成包结构,这是后半本书的骨架。 本篇对应教材 2.1 节,本章八篇覆盖 2.1 到 2.7 全部小节。

一、这本书最后要做成什么

教材在章首先亮出目标:一个能上传图片、调用物件侦测模型、把侦测结果和缩略图存进数据库、 还能对外提供 API 的 Web 应用。 拆开来看是三块界面:普通用户看的上传页与结果页、管理员看的记录列表、给别的程序调的 JSON 接口。 三块的形状完全不同。前两块返回 HTML,第三块返回 JSON;前两块共用一套样式,第三块根本不需要样式。

所以教材的选择很直接:一块界面一个蓝图。

二、为什么不能都写在一个文件里

单文件写法在页面少于五个的时候是舒服的。超过之后,问题不是「文件太长」,是「改一处要翻半天」。 更麻烦的是循环导入。 路由函数要用模型,模型要用数据库对象,数据库对象又要用应用实例——三样放一个文件没事, 拆开就互相 import。 蓝图解决的正是这个:蓝图只依赖自己的模板和函数。谁用它、挂在哪,由工厂函数说了算。

项目 单文件写法 蓝图写法
路由挂在哪 app.route bp.route
地址前缀 写在装饰器里 注册时给
模板目录 整个应用一个 每个蓝图各一个
复用 复制粘贴 注册两次

三、蓝图到底包了什么

一个蓝图对象里装着三样东西:一组路由、一个模板目录、一个静态目录。 建蓝图只有一行:bp = Blueprint("shop", __name__)。 第二个参数和 Flask(__name__) 一个意思,是用来定位模板与静态目录的。 然后照常用 @bp.route("/") 挂路由。函数体、返回值,和挂在 app 上没有任何区别。

最后在工厂里 app.register_blueprint(bp)。这一行发生之前,那些路由对应用来说根本不存在。 这句话要记住:蓝图不是应用,它是一份可以稍后装上去的路由清单。

四、端点名为什么带前缀

实测的路由表里,两个蓝图各有一个叫 index 的函数,端点分别是 shop.index 和 admin.index。 端点名是 url_for 用来找地址的键。同名就会互相覆盖,蓝图自动加了前缀,重名问题自然消失。

url_for("shop.index")   # -> /
url_for("admin.users")  # -> /admin/users

代价是 url_for 里必须写全名。只写 "index" 会报 BuildError——这是用蓝图第一个会踩的坑。

写法 结果
url_for("shop.index") /
url_for("admin.users") /admin/users
url_for("index") BuildError

五、前缀在注册时给,不在定义时写

教材的做法是 shop 蓝图挂在根上,admin 蓝图挂在 /admin 上。 两个前缀都写在 register_blueprint 的参数里,蓝图自己不知道。 好处很实际:换前缀不用改蓝图里任何一行。做多语言站点时,同一个蓝图可以挂 /zh 和 /en。 想给同一个蓝图挂两个前缀,第二次注册要加 name 参数,否则端点名撞车。 Demo 里那个 admin2 就是这么来的。

实测表里能看到 /admin/ 与 /manage/ 两套地址并存,端点一个叫 admin.index、一个叫 admin2.index。

六、蓝图自己的模板与静态目录

默认情况下,Flask 会去蓝图所在的包里找 templates 和 static。 所以每个蓝图可以带自己的页面,互不干扰。

写法 效果
Blueprint("x", name) 模板找 x/templates,静态找 x/static
template_folder="tpl" 改到 x/tpl
static_folder=None 关掉这个蓝图的静态目录

这条规则在多人协作时特别重要。各写各的模板,不会因为「两个人都建了 index.html」打起来。 代价是渲染时只写文件名,不写路径。真要区分,得在各自的 templates 下面再建子目录。

七、动手 Demo:蓝图:把一堆路由装成一个包

这个 Demo 造两个蓝图,一个挂根上一个挂 /admin,然后打印路由表看端点名怎么变。 第三节再把 admin 蓝图挂到第二个前缀上,看同一套页面开出两套地址。

# shop/views/shop.py
from flask import Blueprint
bp = Blueprint("shop", __name__)

@bp.route("/")
def index():
    return "店面首页"

# shop/__init__.py
app.register_blueprint(shop_bp)
app.register_blueprint(admin_bp, url_prefix="/admin")

实跑输出:

--------------------------------------------------------------------
蓝图:把一堆路由装成一个包
--------------------------------------------------------------------

  ① 两个蓝图,各自的端点名前缀
  --------------------------------------------------------------------

| 规则 | 端点 |
|---|---|
| / | shop.index |
| /admin/ | admin.index |
| /admin/users | admin.users |
| /item/<int:iid> | shop.item |
| /static/<path:filename> | static |

  ! 端点名变成了「蓝图名.函数名」。
  所以两个蓝图里都可以有一个叫 index 的函数,不会打架——这是蓝图最基本的作用。

  ② 前缀是注册时给的,不是写死的
  --------------------------------------------------------------------
  GET /              -> 200 店面首页
  GET /item/5        -> 200 商品 5
  GET /admin/        -> 200 后台首页
  GET /admin/users   -> 200 用户管理
  url_for('shop.index') -> /
  url_for('admin.users') -> /admin/users
  ! url_for 里必须带蓝图名。只写函数名会报 BuildError——这是蓝图用起来第一个会踩的错。

  ③ 同一个蓝图挂两个前缀
  --------------------------------------------------------------------

| 规则 | 端点 |
|---|---|
| /admin/ | admin.index |
| /admin/users | admin.users |
| /manage/ | admin2.index |
| /manage/users | admin2.users |

  ! 第二次注册要给 name 参数,否则端点名撞车直接报错。
  这个用法不多,但做「同一套页面挂内外网两套地址」时很好用。

  ④ 蓝图自己的模板与静态目录
  --------------------------------------------------------------------

| 写法 | 效果 |
|---|---|
| Blueprint("x", __name__) | 模板找 x/templates,静态找 x/static |
| template_folder="tpl" | 改到 x/tpl |
| static_folder=None | 关掉这个蓝图的静态目录 |

  默认情况下 Flask 会往「蓝图所在包」下找 templates 和 static,所以每个蓝图可以带自己的页面,
  互不干扰。

路由表里那条 static 是 Flask 给应用自己加的,不是蓝图带来的。 第二张表里 /admin/ 与 /manage/ 同时存在,说明同一份蓝图可以挂两次,第二次必须给 name。 url_for 那一行必须在请求上下文里调,否则报 RuntimeError。

30 秒实验

  1. 把 admin 蓝图的前缀改成 /sys,看路由表里有几条地址跟着变。
  2. 只写 url_for("users") 试一次,看它报的 BuildError 里列了哪些候选端点。
  3. 想一想:如果把两个蓝图都挂在根上,会发生什么?

常见误区

  1. 在蓝图里写死前缀:装饰器写成 @bp.route("/admin/"),前缀和路由规则搅在一起, 换前缀要改每一个函数。
  2. 忘了注册蓝图:路由都写好了,访问还是 404,因为那份清单没装上去。
  3. url_for 里不带蓝图名:报 BuildError,而且报错里不会提示「你少写了前缀」。
  4. 以为蓝图能共享应用级配置:蓝图本身没有 config,配置只能从当前应用取。
自测三问(点开对答案) 1. 蓝图和应用实例的区别是什么? —— 应用是能响应请求的那个东西;蓝图只是一份等着被装上去的路由清单。 2. 端点名为什么自动带蓝图名前缀? —— 不同蓝图里可以有同名函数,加前缀才不会互相覆盖。 3. 蓝图里的模板文件放在哪? —— 蓝图所在包的 templates 目录下,渲染时只写文件名。

下一篇把目录真的搭出来:run.py、工厂函数、模型、模板、静态文件各归位。

相关笔记