想象一个网站:/ 是首页、/about 是关于页、/user/123 是用户 123 的主页。每个 URL 对应一段代码(视图函数)。路由(Routing)就是这张"URL 与代码的对照表"。
Flask 用装饰器实现这个映射——@app.route('/路径') 下面跟的视图函数就是"这个 URL 该执行什么"。这比手动解析 URL 优雅得多,也是 Flask 最核心的 API。
💡 关键直觉:路由 = 网站的"菜单"。客户端点"菜"(URL),路由负责"下单"到正确的厨房(视图函数)。
from flask import Flask app = Flask(__name__) @app.route('/') def index(): return "首页" @app.route('/about') def about(): return "关于我们"
每个固定路径对应一个视图函数。URL 完全匹配才会命中。
@app.route('/user/<username>') def show_user(username): return f"用户: {username}" @app.route('/post/<int:post_id>') def show_post(post_id): return f"文章 ID: {post_id}"
<username> 是字符串参数,<int:post_id> 限定为整数。**转换器(Converter)**控制参数类型:
| 转换器 | 匹配 | 示例 |
|---|---|---|
| string | 任意非斜杠文本 | /user/bob |
| int | 整数 | /post/42 |
| float | 浮点数 | /price/9.5 |
| path | 含斜杠的路径 | /files/a/b.txt |
| uuid | UUID 字符串 | /item/xxx |
@app.route('/files/<path:filepath>') def show_file(filepath): return f"文件路径: {filepath}"
@app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': return "处理登录表单" return "显示登录页面"
默认只接受 GET。用 methods 指定可接受的 HTTP 方法(GET/POST/PUT/DELETE 等)。
硬编码 URL 的坏处:路径一改,所有地方都要跟着改。url_for 按视图函数名生成 URL:
from flask import url_for @app.route('/') def index(): # 生成 /user/bob 这样的 URL url = url_for('show_user', username='bob') return f"用户页面链接: {url}" # 模板里也用:<a href="{{ url_for('show_user', username='bob') }}">Bob</a>
url_for 的价值:路由改名后,所有引用自动更新;模板与代码统一走 url_for,避免路径散落。
⚠️ 常见坑:动态参数类型不匹配。
/post/<int:post_id>访问/post/abc会 404(转换器拒绝非整数)——这是预期行为,不是 bug。想宽松匹配用 string。
/user/123 比 /get_user?id=123 清晰/post/<id>/comment/<cid> 可用,但别过度嵌套from flask import Flask, url_for app = Flask(__name__) @app.route('/') def index(): links = [url_for('show_user', username='bob'), url_for('show_post', post_id=42)] return f"链接: {links}" @app.route('/user/<username>') def show_user(username): return f"用户 {username}" @app.route('/post/<int:post_id>') def show_post(post_id): return f"文章 {post_id}" if __name__ == '__main__': app.run(debug=True)
访问 /user/bob、/post/42、/post/abc(会 404),以及首页看 url_for 生成的链接。
Flask 的路由基于 Werkzeug 的 URL Map:注册路由时生成一棵"规则树",请求进来后按 URL 与方法匹配。理解这层有助于调试:
# 查看应用的所有路由规则 for rule in app.url_map.iter_rules(): print(rule.rule, rule.methods)
输出示例:/ {'GET', 'HEAD', 'OPTIONS'}、/user/<username> {'GET', ...}。调试"路由没生效"时,先查这里——规则是否注册、方法是否匹配。
问:路由顺序有影响吗?
有。Flask 按注册顺序匹配,先注册的先命中。动态路由 /user/<username> 与静态 /user/admin 同时存在时,静态应先注册(或设计上避免冲突)。
问:同一个 URL 能对应多个函数吗?
同一 URL 不同方法可以(如 GET 显示、POST 提交);同 URL 同方法只能有一个视图函数,后注册的覆盖先注册的。
问:路由里能用正则吗?
原生不支持正则转换器,但可自定义转换器(继承 BaseConverter)实现复杂匹配——需要时再学,日常用内置转换器足够。
@app.route('/hello') # 访问 /hello 或 /hello/ 都行(默认允许尾斜杠重定向) @app.route('/hello/', strict_slashes=False) # 两种情况都匹配 @app.route('/api/', strict_slashes=True) # 只有 /api/ 匹配,/api 报 404
默认情况下,/hello 与 /hello/ 是等价的(Flask 自动做 301 重定向)。API 场景常设置 strict_slashes=True 让两端严格区分。
| 转换器 | 匹配内容 | 示例 | 说明 |
|---|---|---|---|
string(默认) |
任意文本(不含斜杠) | <string:name> |
最常用 |
int |
整数 | <int:uid> |
自动转成 int 类型 |
float |
浮点数 | <float:price> |
自动转 float |
path |
含斜杠的路径 | <path:filepath> |
用于文件路径等 |
uuid |
UUID 字符串 | <uuid:token> |
自动校验格式 |
any |
枚举值之一 | <any(a,b,c):page> |
限定可选值 |
@app.route('/files/<path:filepath>') def download(filepath): # 访问 /files/docs/readme.md → filepath='docs/readme.md' return f'下载 {filepath}' @app.route('/color/<any(red,green,blue):c>') def color(c): # 只有 /color/red、/color/green、/color/blue 匹配 return f'选择了 {c}'
每个路由都有一个内部标识——endpoint(默认等于视图函数名):
@app.route('/user/<int:uid>', endpoint='user_detail') def show_user(uid): ... # 模板里用 endpoint 生成 URL,而不硬编码路径 # url_for('user_detail', uid=3) → /user/3
用 endpoint 而不是路径的好处:路径改了(比如 /user/<int:uid> 改成 /member/<int:uid>),模板里 url_for('user_detail', uid=3) 自动跟随,不用到处改字符串——这是"集中管理路由"的核心价值。
视图函数可以返回多种类型,Flask 会自动处理:
| 返回值 | Flask 的处理 |
|---|---|
str |
作为 HTML 正文返回,状态码 200 |
dict |
自动转 JSON |
(值, 状态码) |
指定状态码 |
Response 对象 |
原样返回 |
redirect(...) |
302 跳转 |
abort(404) |
抛出错误,走错误处理 |
app.py 里堆 @app.route;提前知道的教训:不要等路由几百个了才想拆分——那时改造成本已高。每新增一个功能模块,就新建一个蓝图,这是职业级开发的默认习惯。
| 现象 | 原因 | 解法 |
|---|---|---|
| 访问返回 404 | URL 拼错/路由没注册 | 检查路径与 app.run 的 host |
| 访问返回 405 | 方法不匹配 | 确认 methods 是否包含请求方法 |
| 页面报 404 但路由存在 | 蓝图未注册 | 检查 register_blueprint |
| 参数取到的是字符串 | 没用转换器 | <int:uid> 自动转 int |
| url_for 报 BuildError | endpoint 拼错 | 确认 endpoint 名(默认=函数名) |
<int:uid> 等转换器自动类型转换;path 转换器支持斜杠。路由是用户看到的"门牌号",好的 URL 设计让应用更易用、更易维护。几条实践建议:
用名词表示资源,动词靠 HTTP 方法。 这是 REST 的核心思想(第二章 2.8 会系统讲)。/users/1 表示"用户 1 这个资源",GET /users/1 是读取它,DELETE /users/1 是删除它——同一个 URL,方法不同语义不同。这比 /get_user?id=1、/delete_user?id=1 这种动词化 URL 规范得多。
URL 用小写与连字符。 /user-profile 比 /user_profile 更常见、更可读。参数名用 snake_case(Python 惯例)。
保持 URL 简洁、层次清晰。 /posts/<int:pid>/comments 表示"某篇文章的评论",层级关系一目了然。避免过深的嵌套(超过两三层就该考虑扁平化)。
用 url_for 而不是硬编码路径。 这是最重要的习惯——模板、重定向、测试里都通过 endpoint 生成 URL。路径一旦变更,全站自动跟随,不会出现"改了一个路径,五处 404"的惨剧。
一个真实场景:博客系统的路由设计——
GET / 首页,最新文章列表 GET /posts 文章列表(分页) GET /posts/<int:pid> 文章详情 GET /posts/<int:pid>/edit 编辑页(需登录) POST /posts/<int:pid>/edit 提交编辑 POST /posts/<int:pid>/delete 删除(POST 而不是 GET,避免误触发)
注意"删除"用了 POST 而不是 DELETE——浏览器表单只支持 GET/POST,所以实际项目中删除操作常用 POST 提交。这是实践与 REST 理想之间的常见妥协,第二章 2.8 会讨论如何用 API 场景实现更严格的 REST。
路由数量膨胀的信号:当你发现一个文件里路由超过几十个、命名开始重复时,就是该用蓝图拆分的信号了——下一章马上讲。