🧪 第4章 应用程序的规格与准备
📄 一句话概括

images 包和 auth 包平级,各带自己的模板与视图,互不 import。 蓝图挂在 /images 上,内部的 / 就成了 /images/,少尾斜杠会被 308 送过去。 第一版列表页的数据是写死的,先把页面的样子定下来,第 5 章换成数据库。

📝 本章和前面几章的关系

这一篇对应教材 4.2 与 4.3 到 4.3.3:目录架构、登录物件侦测应用、列表页的端点与模板、确认运作。 上一篇是纸上的规格,这一篇把它落成目录和第一条能打开的页面。

一、目录怎么分层

run.py
web/
    __init__.py
    models.py
    auth/
        views.py
        templates/auth/
    images/
        views.py
        templates/images/
    templates/
    views/

和上一章比只多了一个 images 包,位置和 auth 平级。 平级这件事本身就是设计:images 不认识 auth,auth 也不认识 images,两个都只认识 models。 实测的目录树里最上面还多出一个 instance/。 那是 Flask-SQLAlchemy 自己建的:配置写相对地址 sqlite:///app.db,库文件就落到 instance/app.db。 这个目录默认不进仓库。

二、蓝图怎么注册

app.register_blueprint(main_bp)
app.register_blueprint(images_bp, url_prefix="/images")
app.register_blueprint(auth_bp, url_prefix="/auth")

实测注册之后的路由表只有三条:/、/images/、/static/。 auth 那几条这一章还没挂,因为不需要。 /images/ 末尾那个斜杠不能省。 访问 /images 会被 308 重定向到 /images/,实测 Location 是 http://localhost/images/。 308 是永久重定向。 它不是错误,是 Flask 的默认补斜杠行为——浏览器会自动跟过去,用户什么也感觉不到。

三、端点:查数据、挑模板

bp = Blueprint("images", __name__, template_folder="templates")

@bp.route("/")
def index():
    return render_template("images/list.html", rows=ROWS)

视图只有两行:拿数据、渲染模板。数据这一版写死在 views.py 里。 关键是模板不用改。第 5 章把 ROWS 换成数据库查询,只要还是「一个能遍历的列表」, 模板一个字都不用动。 这就是「先做界面后接数据」的唯一好处:两边可以在不同时间完成。

四、模板:表格与空状态

{% extends "base.html" %}
{% block body %}
<h2>图片列表(共 {{ rows|length }} 张)</h2>
<table border=1>
<tr><th>ID</th><th>文件名</th><th>侦测件数</th><th>上传时间</th></tr>
{% for r in rows %}
<tr><td>{{ r.id }}</td><td>{{ r.name }}</td>
<td>{{ r.n }}</td><td>{{ r.at }}</td></tr>
{% endfor %}
</table>
{% endblock %}

rows|length 那个过滤器直接给出张数,不用在视图里数一遍。 实测有数据时渲染出三行,表头下面排得整整齐齐。 把 rows 换成空列表之后,页面照常 200,只剩一个表头和「共 0 张」。

这是最容易漏的一个状态。开发时手里总有数据,上线第一天就空了——空列表要么给一句「还没有图片」, 要么给一个上传入口。

五、404 归到自订页面

处理器写在 create_app 里,不是写到某个蓝图里。 实测 GET /images/nope 返回 404,页面上是我们自己那张「找不到这一页」。 为什么不能写进蓝图:404 的成因是「没有任何规则匹配」,那一刻 Flask 还没走到蓝图, 自然不知道该用哪个蓝图的处理器。

蓝图里那个能生效的写法叫 @bp.app_errorhandler——名字已经说明它注册到应用上。

六、确认运作

三条地址一起过一遍:/ 返回 200,/images/ 返回 200,/images/nope 返回 404。 教材 4.3.3 的「确认运作」就是这一步。看着简单,但它是这一章的交付标准。 每写完一页就过一遍这三条,比全部写完再一起调试省好几倍时间。

七、动手 Demo:目录架构与第一版列表页

这个 Demo 按上一篇的规格搭目录:images 包、蓝图注册、列表页、404 页面。 第三节看有数据的列表,第四节把数据清空,看空列表长什么样。

bp = Blueprint("images", __name__, template_folder="templates")

ROWS = [
    {"id": 1, "name": "cat.jpg", "n": 3, "at": "2026-11-01"},
    {"id": 2, "name": "dog.png", "n": 1, "at": "2026-11-02"},
    {"id": 3, "name": "street.jpg", "n": 7, "at": "2026-11-03"},
]

@bp.route("/")
def index():
    return render_template("images/list.html", rows=ROWS)

实跑输出:

--------------------------------------------------------------------
目录架构与第一版列表页
--------------------------------------------------------------------

  ① 目录长这样
  --------------------------------------------------------------------
  ├── instance/
  ├── run.py
  └── web/
      ├── __init__.py
      ├── auth/
      │   ├── __init__.py
      │   ├── templates/
      │   │   └── auth/
      │   │       └── x.html
      │   └── views.py
      ├── images/
      │   ├── __init__.py
      │   ├── templates/
      │   │   └── images/
      │   │       └── list.html
      │   └── views.py
      ├── models.py
      ├── templates/
      │   ├── 404.html
      │   ├── base.html
      │   └── index.html
      └── views/
          ├── __init__.py
          └── main.py
  ! 和上一章比,多了一个 images 包。它和 auth 是平级的,谁都不认识谁——这正是蓝图分模块想要的结果。
  ! 最上面那个 instance/ 是 Flask-SQLAlchemy 自己建的:配置里写相对路径 sqlite:///app.db,
  文件就会落到 instance/app.db,而不是项目根目录。这个目录默认不进仓库。

  ② 路由表
  --------------------------------------------------------------------

| 规则 | 端点 |
|---|---|
| / | main.index |
| /images/ | images.index |
| /static/<path:filename> | static |

  GET /images(不带尾斜杠)-> 308,Location=http://localhost/images/
  ! images 蓝图挂在 /images 上,它内部的 / 就变成 /images/。
  少了尾斜杠会被 308 送到带斜杠的地址,这是 Flask 的默认行为,不是配置出来的。

  ③ 第一版列表页
  --------------------------------------------------------------------
  GET /images/ -> 200
  <h2>图片列表(共 3 张)</h2>
  <tr><th>ID</th><th>文件名</th><th>侦测件数</th><th>上传时间</th></tr>
  <tr><td>1</td><td>cat.jpg</td><td>3</td><td>2026-11-01</td></tr>
  <tr><td>2</td><td>dog.png</td><td>1</td><td>2026-11-02</td></tr>
  <tr><td>3</td><td>street.jpg</td><td>7</td><td>2026-11-03</td></tr>
  ! 三行数据是写死在 views.py 里的。
  这一章先让页面长出来,第 5 章把 ROWS 换成数据库查询,模板一个字都不用改。

  ④ 列表为空时页面长什么样
  --------------------------------------------------------------------
  rows 变成空列表之后:
    <h2>图片列表(共 0 张)</h2>
    <table border=1>
  返回码还是 -> 200
  ! 空列表不报错,页面照常 200,只是表头下面什么都没有。
  这是最容易漏的一个状态——开发时手里总有数据,上线第一天就空了。

  ⑤ 404 归到自订页面
  --------------------------------------------------------------------
  GET /images/nope -> 404
    <h2>找不到这一页</h2>
  ! 404 处理器挂在应用上,不是挂在蓝图上。
  这一点实测过:在蓝图里写 @bp.errorhandler(404) 不会生效,因为 404 是「没有任何规则匹配」,
  那一刻 Flask 还不知道该归哪个蓝图。
  ! 蓝图里能用的那个叫 @bp.app_errorhandler,名字就是答案——它注册到应用上,
  和直接把处理器写在 create_app 里等价。

  ⑥ 三条地址一起确认
  --------------------------------------------------------------------
  GET /                -> 200
  GET /images/         -> 200
  GET /images/nope     -> 404

第一节那棵树里 auth 和 images 是平级的两块,这就是分模块的样子。 第二节那条 308 是补斜杠,不是错误——浏览器会自动跟过去。 第四节清空数据之后,返回码还是 200,只是表里什么都没有。这一节值得多看两眼。

30 秒实验

  1. 把列表页的地址从 / 改成 /list,访问 /images/ 看会发生什么。
  2. 给空列表加一段 {% if not rows %} 的分支,写一句「还没有图片」。
  3. 想一想:如果把 images 的视图写进 main 蓝图,目录会变成什么样?

常见误区

  1. 蓝图前缀写成 /images/(带尾斜杠):规则会变成 /images//,访问时 404。
  2. 模板里忘了 |length:写成 rows.length,Jinja 会当成字典取值,渲染成空。
  3. 只测有数据的情况:空列表那一条从来不点,上线当天才发现是白板。
  4. 错误页写进蓝图:不生效,且不报错。
自测三问(点开对答案) 1. images 与 auth 为什么平级? —— 两者互不依赖,各自只认识 models,平级才不会被对方的改动牵连。 2. 访问 /images 时的 308 是什么? —— Flask 自动补尾斜杠的永久重定向,浏览器会跟过去。 3. 列表页的数据换成数据库查询时,模板要改吗? —— 不用,只要还是能遍历的列表,模板一个字都不动。

第 4 章到此结束。下一篇写真东西:UserImage 模型、关联关系与真的列表查询。

相关笔记