第 2 章 · 路由、蓝图与请求上下文
本章目标:掌握 Flask Blueprint(蓝图)拆分路由模块;熟练使用 url_for 生成 URL 避免硬编码;理解 request 与 response 对象读写查询参数、表单与 JSON;认识 g 请求级全局对象与 before_request 钩子;在 api-demo 中实现 main 与 api 两套蓝图,为 REST 章节打基础。
学时建议:4~5 小时(含 1.5 小时跟练)
前置:完成 flask-web ch01(create_app、最小路由、flask run)。
2.1 场景说明:拆分「页面」与「API」
api-demo 需要同时提供:
| 访问方 | 路径前缀 | 蓝图 | 响应类型 |
|---|---|---|---|
| 运维探活 | /health | main | JSON |
| 管理页(ch03) | /admin/... | admin | HTML |
| 对外 REST | /api/v1/... | api | JSON |
| 虚构公网入口 | https://api.example.com | 反代到本服务 | — |
若所有 @app.route 写在 __init__.py,文件会迅速膨胀。蓝图按功能模块组织,类似 Django 的 App + urls.py。
api_demo/
├── routes/
│ ├── main.py # 根与健康检查
│ ├── api.py # /api/v1/products 等
│ └── admin.py # /admin/(ch03 模板)
2.2 蓝图 Blueprint 基础
| 概念 | Django 对照 | Flask 蓝图 |
|---|---|---|
| 模块划分 | catalog App | api Blueprint |
| URL 前缀 | path("catalog/", include(...)) | Blueprint(..., url_prefix="/api/v1") |
| 命名空间 | app_name + name | Blueprint 名 + 端点名 |
api_demo/routes/api.py:
from flask import Blueprint, jsonify, request
bp = Blueprint("api", __name__, url_prefix="/api/v1")
@bp.route("/products", methods=["GET"])
def product_list():
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 10, type=int)
items = [
{"id": 1, "name": "示例商品 A", "price": 99.0},
{"id": 2, "name": "示例商品 B", "price": 199.0},
]
return jsonify({
"page": page,
"per_page": per_page,
"count": len(items),
"items": items,
})
@bp.route("/products/<int:product_id>", methods=["GET"])
def product_detail(product_id):
if product_id == 1:
return jsonify({"id": 1, "name": "示例商品 A", "price": 99.0})
return jsonify({"error": "not_found"}), 404
api_demo/routes/admin.py(占位,ch03 接模板):
from flask import Blueprint
bp = Blueprint("admin", __name__, url_prefix="/admin")
@bp.route("/")
def dashboard():
return "Admin dashboard (templates in ch03)"
更新 api_demo/__init__.py:
from flask import Flask
from api_demo.config import config_map
def create_app(config_name=None):
if config_name is None:
config_name = "default"
app = Flask(__name__)
app.config.from_object(config_map[config_name])
from api_demo.routes.main import bp as main_bp
from api_demo.routes.api import bp as api_bp
from api_demo.routes.admin import bp as admin_bp
app.register_blueprint(main_bp)
app.register_blueprint(api_bp)
app.register_blueprint(admin_bp)
return app
2.3 url_for 动态 URL
硬编码 href="/api/v1/products" 在修改前缀时需全局替换。url_for 根据端点名生成 URL。
端点命名规则:{蓝图名}.{视图函数名},例如 api.product_list。
from flask import url_for
list_url = url_for("api.product_list") # /api/v1/products
detail_url = url_for("api.product_detail", product_id=1)
api_demo/routes/main.py 增加链接演示:
from flask import Blueprint, url_for
bp = Blueprint("main", __name__)
@bp.route("/")
def index():
products_api = url_for("api.product_list")
admin_home = url_for("admin.dashboard")
return (
f"api-demo running.<br>"
f"Products API: {products_api}<br>"
f"Admin: {admin_home}"
)
@bp.route("/health")
def health():
return {
"status": "ok",
"service": "api-demo",
"endpoints": {
"products": url_for("api.product_list", _external=True),
},
}
| 参数 | 作用 |
|---|---|
_external=True | 生成完整 URL(含主机),用于 api.example.com 文档 |
_method="POST" | 指定方法生成对应规则 |
2.4 request 对象
request 是线程局部的上下文对象,每个 HTTP 请求独立。
| 属性/方法 | 用途 | 示例 |
|---|---|---|
request.method | GET / POST 等 | if request.method == "POST" |
request.args | 查询参数 | request.args.get("page", 1, type=int) |
request.form | 表单字段 | request.form["name"] |
request.json | JSON 体(需 Content-Type) | request.get_json() |
request.headers | 请求头 | request.headers.get("Authorization") |
request.path | 路径 | /api/v1/products |
测试分页:
curl "http://127.0.0.1:5000/api/v1/products?page=2&per_page=5"
2.5 response 与 jsonify
| 方式 | 适用 |
|---|---|
| 返回字符串 | 简单文本 |
| 返回 dict / list | Flask 自动 JSON 化(2.2+) |
jsonify(data) | 明确 JSON 响应,可设状态码 |
make_response() | 自定义头、Cookie |
redirect(url_for(...)) | 302 跳转 |
from flask import jsonify, redirect, request, url_for
@bp.route("/products", methods=["POST"])
def product_create():
data = request.get_json(silent=True) or {}
name = data.get("name")
if not name:
return jsonify({"error": "name_required"}), 400
return jsonify({"id": 99, "name": name}), 201
@bp.route("/legacy-products")
def legacy_redirect():
return redirect(url_for("api.product_list"))
状态码放在 return 元组第二位:return body, 404。