这一章不写业务代码,只把六张页面的地址与字段定下来。 字段表是数据库表的上游:先定页面要显示什么,再定表要存什么。 错误页也算一张页面。它不占地址,但不写的话用户看到的是英文默认页。
这一篇对应教材 4.1 到 4.1.6:规格、图片列表、侦测、上传、物件侦测、搜寻、自订错误页。 这是全书唯一一章不产生新代码的,但它同时是后面八章的地图——写代码卡住时回来看它。
一、为什么不直接开写
前面的章节每一步都有明确目标:搭骨架、接数据库、加登录。 到这里忽然要做一个「真应用」,第一反应往往是先建模型再说。 教材的选择是先写规格:六张页面、每张要显示什么、走什么方法。一页纸写完,再动手。 理由很实在:字段改起来,越晚越贵。库建好了再改字段要写迁移,模板写好了再改字段要重排页面, 两样都比在纸上改一个字贵。
所以这一章看着像「废话」,实际是在省后面的返工。
二、六张页面与它们的地址
| 页面 | 地址 | 方法 |
|---|---|---|
| 图片列表 | /images/ | GET |
| 图片详情 | /images/ |
GET |
| 上传图片 | /images/upload | GET 与 POST |
| 执行侦测 | /images/ |
POST |
| 图片搜寻 | /images/search | GET |
| 错误页 | (不占地址) | — |
地址里那个 <int:iid> 是路由变量,第 1 章讲过。
用 int 转换器有个好处:/images/abc 会直接 404,不会进到视图里再报类型错。
「执行侦测」挂 POST,因为它会改数据(写入侦测结果)。这条规矩前面已经用过两次:删除、登出。
上传页一个地址两种方法:GET 给表单,POST 收文件。这是第 2 章 2.6 那个写法的直接复用。
三、字段表:数据库的上游
教材给每张页面列了要显示的字段。六页加起来 23 个字段,平均每页不到 4 个。 看着琐碎,但它回答了一个关键问题:这张表到底要存什么。页面上要显示的,库里必须有; 库里有的,不一定每页都显示。 比如「侦测件数」在列表页和详情页都要显示。 它有两种存法:存成 UserImage 的一个列,或者每次去数结果表。
存列快但会不一致,去数慢但永远是对的。教材后面选了后者——第 5 章的关联查询就是为它准备的。 这类取舍在写代码之前想清楚,比写完再改省事得多。
四、错误页也是一张页面
4.1.6 单独列了自订错误页。它不占地址,但用户一定会撞上——链接点错、记录被删、手打地址打错。 实测:没挂处理器时,404 返回的是 207 字节的英文默认页; 自己写一个,76 字节,至少告诉用户该点哪儿回去。 关键一条:状态码必须是 404,不能改成 200。改成 200 的话, 搜索引擎会把一堆错误页当正常页面收录。
顺手记一个坑:这个处理器只能挂在应用上。
在蓝图里写 @bp.errorhandler(404) 实测不生效,因为 404 是「没有任何规则匹配」,
那一刻 Flask 还没确定该归哪个蓝图。
五、规格写完之后怎么用
把那张地址表当清单:每写完一个端点就划掉一行。八个章节之后,六行全划完,应用也就做完了。 把那张字段表当检查表:写模型时对着它一列一列过,不容易漏。 规格最大的价值不是「预先设计」,是「随时能核对」——写到一半发现页面缺点东西,回来补一行, 比写到结尾再推翻重来便宜。
六、动手 Demo:六个页面的规格:字段、地址与错误页
这个 Demo 把那张规格表变成能跑的东西:六条地址先立成空壳,逐个访问看返回码。 最后一节对比默认 404 与自订 404 的体量。
PAGES = [
("图片列表", "/images/", "GET", [...]),
("图片详情", "/images/<int:iid>", "GET", [...]),
("上传图片", "/images/upload", "GET 与 POST", [...]),
("执行侦测", "/images/<int:iid>/detect", "POST", [...]),
("图片搜寻", "/images/search", "GET", [...]),
]
@app.errorhandler(404)
def not_found(e):
return render_template("404.html"), 404
实跑输出:
--------------------------------------------------------------------
六个页面的规格:字段、地址与错误页
--------------------------------------------------------------------
① 六张页面各自的地址
--------------------------------------------------------------------
| 页面 | 地址 | 方法 |
|---|---|---|
| 图片列表 | /images/ | GET |
| 图片详情 | /images/<int:iid> | GET |
| 上传图片 | /images/upload | GET 与 POST |
| 执行侦测 | /images/<int:iid>/detect | POST |
| 图片搜寻 | /images/search | GET |
| 错误页 | (不占地址) | — |
! 先定地址再写代码,好处是「谁调谁」在纸上就定了。
第 5 章以后每加一个功能,都是往这张表里补一行。
② 每张页面要显示什么字段
--------------------------------------------------------------------
| 页面 | 字段 |
|---|---|
| 图片列表 | id、文件名、缩略图、侦测件数、上传时间、操作 |
| 图片详情 | id、文件名、原图、侦测框、侦测件数、上传者 |
| 上传图片 | 图片文件、备注 |
| 执行侦测 | id、模型名、阈值 |
| 图片搜寻 | 关键字、起日、讫日 |
| 错误页 | 状态码、说明、回首页链接 |
六张页面一共 23 个字段,平均每页 3.8 个
! 这张表是「数据库要存什么」的上游。
字段一旦定死,模型和迁移就照着它写——反过来先写模型再想页面,一定会漏字段。
③ 地址能不能都立起来
--------------------------------------------------------------------
/images/ -> 200
/images/7 -> 200
/images/upload -> 200
/images/7/detect -> 200
/images/search -> 200
/images/nope -> 404
! /images/nope 是 404,因为它在规格里根本不存在。
规格的用处之一就是:不该通的地址,通不了是对的。
④ 自订错误页顶掉默认那一页
--------------------------------------------------------------------
没挂错误处理器时,404 页面 207 字节
挂上之后,同样一个地址 76 字节
自定义页面的内容 -> <h1>找不到这一页</h1><p>地址可能打错了,回首页看看。</p>
! 默认那一页是英文的、只有一行。换成自己的,至少能让用户知道该点哪儿回去。
! 实测这一收一发只改了 404 的响应体,状态码还是 404——不能改成 200,
否则搜索引擎会把错误页当成正常页面收录。
! 这一节要建两个应用实例,是因为 Flask 2.3 之后禁止在「处理过第一个请求」之后再注册错误处理器。
同一件事在真实项目里不会遇到——错误页总是在 create_app 里一次注册好。
第三节那六行里,只有 /images/nope 是 404——它在规格里本来就不该存在。 第四节默认页 207 字节、自订页 76 字节,差别不在长短,在「看不看得懂」。 那个报错式的注记是实情:Flask 2.3 之后不允许在处理过请求之后再注册错误处理器, 所以这一节建了两个应用实例。
30 秒实验
- 给规格表再加一张「侦测结果详情」页面,想想它需要哪些字段、地址怎么排。
- 把错误页的状态码改成 200,用浏览器的网络面板看它变成什么颜色。
- 想一想:为什么「侦测件数」存成列会有不一致的风险?
常见误区
- 先建模型再想页面:写到第三张页面才发现少一个字段,改模型要迁移、改模板要重排。
- 错误页返回 200:搜索引擎会把错误页收进索引,用户搜到的全是空页面。
- 侦测这种改数据的操作用 GET:挂 POST 的理由和删除、登出完全一样。
- 在蓝图里注册 404:不生效,而且不报错,表现为「我写的错误页怎么不出来」。
自测三问(点开对答案)
1. 为什么这一章只写规格不写代码? —— 字段改得越晚越贵,先在纸上定死能省后面的返工。 2. 「侦测件数」存列与现数各有什么代价? —— 存列快但可能不一致,现数慢但永远是对的。 3. 自订错误页为什么要保持 404? —— 改成 200 会被搜索引擎当正常页面收录。规格定了,下一篇按它搭目录:images 包、蓝图注册、第一版列表页。
- ☐ (本篇完,下一篇 4.2 目录架构与第一版列表页)