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//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 秒实验
- 把列表页的地址从 / 改成 /list,访问 /images/ 看会发生什么。
- 给空列表加一段
{% if not rows %}的分支,写一句「还没有图片」。 - 想一想:如果把 images 的视图写进 main 蓝图,目录会变成什么样?
常见误区
- 蓝图前缀写成 /images/(带尾斜杠):规则会变成 /images//,访问时 404。
- 模板里忘了 |length:写成 rows.length,Jinja 会当成字典取值,渲染成空。
- 只测有数据的情况:空列表那一条从来不点,上线当天才发现是白板。
- 错误页写进蓝图:不生效,且不报错。
自测三问(点开对答案)
1. images 与 auth 为什么平级? —— 两者互不依赖,各自只认识 models,平级才不会被对方的改动牵连。 2. 访问 /images 时的 308 是什么? —— Flask 自动补尾斜杠的永久重定向,浏览器会跟过去。 3. 列表页的数据换成数据库查询时,模板要改吗? —— 不用,只要还是能遍历的列表,模板一个字都不动。第 4 章到此结束。下一篇写真东西:UserImage 模型、关联关系与真的列表查询。
- ☐ (本篇完,下一篇 5.1 UserImage 模型与列表端点)