路由(Routing)艺术:变量规则、HTTP 方法与唯一 URL

📂 所属阶段:第一阶段 — 破冰启航(基础篇)
🔗 相关章节:初识 Flask · Jinja2 模板引擎


路由,就是把浏览器请求的 URL 路径,和我们写的 Python 处理函数一对一绑定的机制。Flask 的路由系统灵活、优雅,是它入门友好的核心原因之一。今天我们就把这个基础但核心的功能啃透。


1. 从最简单的路由开始

1.1 基础装饰器写法

Flask 用 @app.route() 装饰器来定义路由,装饰器的参数就是要绑定的 URL 路径。我们先写一段最直观的 Hello Flask 风格代码:

from flask import Flask, jsonify

# 初始化 Flask 应用
app = Flask(__name__)

# 绑定根路径 /
@app.route("/")
def index():
    return "<h1>欢迎来到道满的 Flask 技术站!</h1>"

# 绑定 /about 路径
@app.route("/about")
def about():
    return "<p>这里是道满,专注于简洁好用的 Web 技术</p>"

if __name__ == "__main__":
    app.run(debug=True, port=5001)

启动这段代码,访问 http://127.0.0.1:5001http://127.0.0.1:5001/about 就能看到对应的页面了。

1.2 装饰器的“糖衣”原理(简单版)

很多新手可能会觉得装饰器很神秘,但在 Flask 路由里,它只是语法糖——本质是把函数塞进 app 的路由映射表。

# 上面的 @app.route("/") 等价于下面这段原生 Python:
def unwrapped_index():
    return "<h1>欢迎来到道满的 Flask 技术站!</h1>"

# 调用 app.route() 先拿到一个「函数改造器」
# 再把改造器应用到 unwrapped_index 上,覆盖原函数名
index = app.route("/")(unwrapped_index)

知道这个原理就够了,平时我们还是用装饰器,代码更清晰。


2. 动态路由:让 URL 带“变量”

静态的 /about 只能展示固定内容,要是想展示不同用户的个人页、不同 ID 的文章,总不能写几千个 @app.route("/user/john") 吧?这时候就需要路径变量(动态路由)

2.1 三种最常用的变量写法

from flask import Flask

app = Flask(__name__)

# 1. 默认是字符串(不含斜杠)
@app.route("/user/<username>")
def show_user(username):
    # 变量名会自动传给处理函数当参数
    return f"<h1>用户资料页:{username}</h1>"

# 2. int 类型(自动转成整数,不是整数会 404)
@app.route("/post/<int:post_id>")
def show_post(post_id):
    return f"<h1>博客文章 ID:{post_id}</h1>"

# 3. path 类型(可以带斜杠,适合文件路径、多级分类)
@app.route("/file/<path:file_path>")
def show_file(file_path):
    return f"正在访问文件:{file_path}"

if __name__ == "__main__":
    app.run(debug=True)

2.2 完整变量类型速查表

转换器说明匹配示例 URL
string默认,不含斜杠的任意字符串/user/john_doe
int正整数(自动转换类型)/post/123
float正浮点数(自动转换类型)/product/rate/4.9
path含斜杠的任意字符串(类似 string 但允许 //assets/css/style.css
uuid严格匹配 UUID 格式(自动转换为 Python 的 uuid.UUID 类型)/api/item/550e8400-e29b-41d4-a716-446655440000
any指定固定的几个值(二选一/多选一)/order/<any(pending,completed):status>

其中 any 转换器适合处理「固定选项但不想写多个路由」的场景:

@app.route("/order/<any(pending, completed, canceled):status>")
def show_order_list(status):
    return f"正在查看状态为「{status}」的订单列表"

# ✅ 访问 /order/pending → 正常
# ✅ 访问 /order/canceled → 正常
# ❌ 访问 /order/shipping → 404 (不在 any 指定的范围内)

2.3 多个变量组合(博客归档示例)

变量可以任意组合,只要符合 URL 规范就行:

from flask import Flask
from datetime import datetime

app = Flask(__name__)

@app.route("/blog/<int:year>/<int:month>/<slug>")
def blog_archive(year, month, slug):
    # 验证一下日期的合法性(可选,避免非法日期如 2026/13)
    try:
        target_date = datetime(year, month, 1)
    except ValueError:
        return "日期格式不正确!", 400
    return f"""
        <h1>{target_date.strftime('%Y年%m月')}的文章</h1>
        <p>文章标题链接:{slug}</p>
    """

if __name__ == "__main__":
    app.run(debug=True)

3. HTTP 方法:控制请求的“动作”

浏览器默认的请求方法是 GET,但 Web 开发里还有 POSTPUTDELETE 等——这些方法本质是告诉服务器“你要对这个 URL 对应的资源做什么”。

3.1 多方法绑定(登录表单示例)

登录页面需要两个动作:用 GET 展示表单,用 POST 提交表单。我们可以用 methods 参数绑定多个方法:

from flask import Flask, render_template, request, redirect, url_for, flash

app = Flask(__name__)
# flash 需要 secret_key 加密 session,随便设一个就行
app.secret_key = "your-secret-key-keep-it-safe-in-production"

@app.route("/login", methods=["GET", "POST"])
def login():
    # request.method 可以获取当前请求的方法
    if request.method == "POST":
        # 从 request.form 获取 POST 表单数据
        username = request.form.get("username")
        password = request.form.get("password")
        # 这里省略数据库验证逻辑,直接模拟成功
        flash("登录成功!")
        # 登录成功后跳转到首页(用下一节讲的 url_for)
        return redirect(url_for("index"))
    # GET 请求就展示登录表单
    return render_template("login.html")

@app.route("/")
def index():
    return render_template("index.html")

if __name__ == "__main__":
    app.run(debug=True)

3.2 RESTful 风格的路由设计

REST 是目前最流行的 API 设计原则,核心是“URL 描述资源,HTTP 方法描述动作”。比如对“文章”这个资源:

动作传统 URL(坏例子)RESTful URL(好例子)HTTP 方法
展示文章列表/get_articles/articlesGET
展示单篇文章详情/show_article?id=123/articles/123GET
展示创建文章的表单/create_article_form/articles/newGET
提交创建文章的请求/submit_article/articlesPOST
展示编辑文章的表单/edit_article_form?id=123/articles/123/editGET
更新单篇文章/update_article/articles/123PUT/PATCH
删除单篇文章/delete_article?id=123/articles/123DELETE

注意:浏览器原生只支持 GETPOST,如果要在 HTML 里用 PUT/PATCH/DELETE,需要用 <input type="hidden" name="_method" value="PUT"> 加上 Flask 的 methodoverride 中间件(这里暂时不展开,后面扩展阅读会提)。


4. URL 反向生成:告别硬编码的烦恼

硬编码 URL 是开发中的大忌——比如你把 /about 改成了 /about-us,所有引用这个 URL 的地方(HTML 链接、Python 重定向)都要改,很容易漏。Flask 提供了 url_for 函数,通过处理函数的名字生成 URL,完美解决这个问题。

4.1 url_for 的基础用法

from flask import Flask, url_for, redirect

app = Flask(__name__)

@app.route("/")
def index():
    # 测试在代码里生成 URL
    test_url1 = url_for("show_user", username="alice")  # → /user/alice
    test_url2 = url_for("blog_archive", year=2026, month=3, slug="flask-routing")  # → /blog/2026/3/flask-routing
    return f"""
        <h1>道满的 Flask 技术站</h1>
        <a href='{url_for("about")}'>关于我</a><br>
        <a href='{test_url1}'>Alice 的个人页</a><br>
        <a href='{test_url2}'>Flask 路由教程</a><br>
    """

@app.route("/about")
def about():
    return "关于道满"

@app.route("/user/<username>")
def show_user(username):
    return f"用户资料页:{username}"

@app.route("/blog/<int:year>/<int:month>/<slug>")
def blog_archive(year, month, slug):
    return f"{year}{month}月的文章:{slug}"

# 跳转到某个页面
@app.route("/goto-alice")
def goto_alice():
    return redirect(url_for("show_user", username="alice"))

if __name__ == "__main__":
    app.run(debug=True)

4.2 在 Jinja2 模板中用 url_for

实际开发中,HTML 链接大多写在 Jinja2 模板里,用法和 Python 代码里几乎一样:

<!-- templates/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>道满的 Flask 技术站</title>
</head>
<body>
    <h1>道满的 Flask 技术站</h1>
    <!-- 生成关于页的链接 -->
    <a href="{{ url_for('about') }}">关于我</a><br>
    <!-- 生成带变量的链接 -->
    <a href="{{ url_for('show_user', username='bob') }}">Bob 的个人页</a><br>
    <!-- 生成带额外查询参数的链接(自动拼在 ? 后面) -->
    <a href="{{ url_for('blog_archive', year=2026, month=3, slug='flask-routing', sort='latest') }}">
        最新的 Flask 路由教程
    </a>
</body>
</html>

5. 唯一 URL:Flask 的“智能”尾部斜杠处理

你有没有注意到,访问 https://flask.palletsprojects.com/https://flask.palletsprojects.com 是一样的,但访问某个具体页面的 /about//about 可能不一样?Flask 在这方面做了标准化处理,帮我们避免了“同一个页面有两个 URL”的 SEO 问题。

5.1 Flask 的默认行为

from flask import Flask

app = Flask(__name__)

# 路由没有尾部斜杠 → 类似文件
@app.route("/about")
def about():
    return "关于页面(没有尾部斜杠)"

# 路由有尾部斜杠 → 类似文件夹
@app.route("/blog/")
def blog_list():
    return "博客列表(有尾部斜杠)"

if __name__ == "__main__":
    app.run(debug=True)

测试一下:

  1. 访问 /about → 正常返回
  2. 访问 /about/ → Flask 自动 301 重定向到 /about(保证唯一 URL)
  3. 访问 /blog/ → 正常返回
  4. 访问 /blog → Flask 自动 301 重定向到 /blog/(保证唯一 URL)

5.2 要不要加尾部斜杠?

建议:

  • 静态页面(如 /about/contact)→ 不加,符合文件的直觉
  • 列表/集合页面(如 /blog//users/)→ ,符合文件夹的直觉
  • API 接口(如 /api/articles/api/users/123)→ 统一不加,更简洁

6. 小结

今天我们学了 Flask 路由的所有核心功能:

  1. 基础装饰器@app.route("/path") 绑定 URL 和处理函数
  2. 动态路由<converter:var> 灵活带变量
  3. HTTP 方法methods=["GET", "POST"] 控制请求动作,尝试 RESTful 设计
  4. 反向生成url_for("handler", arg=val) 告别硬编码
  5. 唯一 URL:理解 Flask 的尾部斜杠重定向

💡 核心设计原则:URL 应该是可预测的、描述资源的、唯一的


🔗 扩展阅读