码上问学问代码之理,学编程之道

第 2 章 · 路由

上一章那个 / 只是个门牌。真实的「古典文学网」后端有十几个接口:作品列表、作品详情、书评、收藏、登录……把「哪条 URL 交给哪段代码处理」这件事安排明白,就是路由。本章你会写出带参数的动态路由、用 HTTP 方法区分动作、用 url_for 反推地址,并知道一个尾部斜杠能坑掉你半小时。

一条 URL 对应一个函数

@app.route 到底干了什么
@app.route 做的事:URL → 函数 GET / index() GET /article/<int:id> detail(id) POST /api/user/login login() URL 是「地址」,函数是「动作」;匹配上谁,Flask 就调用谁

路由的本质是一张「URL → 函数」的对照表。请求进来后,Flask 按 URL 找到对应函数并调用,把函数返回值发回浏览器。

先讲为什么餐厅里客人喊「宫保鸡丁」,后厨某个师傅接单——不能所有菜都让同一个师傅做,那样必然乱套。后端也一样:浏览器发来的请求带着一条路径(/api/banner/list),服务器必须知道「这条路径该交给哪个函数处理」。
这个「路径 → 函数」的对照表,就是路由表(URL Map)。前端只需要记住接口文档里的路径,后端只需要保证每个路径都有人接单。
动手做继续用上一章的 app.py,把首页轮播图和分类列表两个接口加上去。每个接口就是一个函数,上面盖一个 @app.route 的章。
app.pypython
from flask import Flask, jsonify

app = Flask(__name__)
app.json.ensure_ascii = False


@app.route("/")
def index():
    return jsonify({"msg": "古典文学网 API 已启动"})


@app.route("/api/banner/list")
def banner_list():
    """首页轮播图:对应 banner 表的三条种子数据"""
    return jsonify({
        "code": 200,
        "msg": "success",
        "data": [
            {"id": 1, "title": "品读古典文学,涵养中国心灵",
             "image": "/static/images/banner1.jpg"},
            {"id": 2, "title": "一场跨越千年的对话",
             "image": "/static/images/banner2.jpg"},
            {"id": 3, "title": "写下你的书评,与同好相遇",
             "image": "/static/images/banner3.jpg"},
        ],
    })


@app.route("/api/category/list")
def category_list():
    """分类列表:对应 category 表的种子数据"""
    return jsonify({
        "code": 200,
        "msg": "success",
        "data": [
            {"id": 1, "name": "唐诗", "description": "唐代诗歌,格律严谨、气象恢宏"},
            {"id": 2, "name": "宋词", "description": "宋代词作,婉约与豪放并存"},
            {"id": 3, "name": "元曲", "description": "元代散曲与杂剧,通俗生动"},
            {"id": 4, "name": "散文", "description": "先秦至明清的散文名篇"},
            {"id": 5, "name": "小说", "description": "章回体与笔记体小说"},
        ],
    })


if __name__ == "__main__":
    app.run(debug=True)
运行输出Python 运行结果
运行结果: $ curl -s http://127.0.0.1:5000/api/banner/list | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": [ { "id": 1, "title": "品读古典文学,涵养中国心灵", "image": "/static/images/banner1.jpg" }, { "id": 2, "title": "一场跨越千年的对话", "image": "/static/images/banner2.jpg" }, { "id": 3, "title": "写下你的书评,与同好相遇", "image": "/static/images/banner3.jpg" } ] } $ curl -s http://127.0.0.1:5000/api/category/list | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": [ { "id": 1, "name": "唐诗", "description": "唐代诗歌,格律严谨、气象恢宏" }, { "id": 2, "name": "宋词", "description": "宋代词作,婉约与豪放并存" } ] }
讲透:一个请求走完的三步
  • 第一步 URL → 视图函数:Flask 拿着请求路径去路由表里找,找到就交给对应函数。找不到就返回 404,根本不进你的代码。所以「接口返回 404」通常不是函数写错了,而是路径对不上。
  • 第二步 视图函数干活:查库、校验参数、做业务判断。这一步以后会越来越重。
  • 第三步 返回值 → 响应:函数返回什么,用户就收到什么。jsonify() 返回 JSON,返回字符串则是纯文本;返回 (内容, 状态码) 元组可以指定 HTTP 状态码(第 3 章细讲)。
  • 装饰器的作用就是登记:@app.route("/api/banner/list") 这一行执行时,Flask 就把「/api/banner/list」和下面的 banner_list 记进了路由表。函数名本身是什么不重要,装饰器里的路径才是门牌号。
  • 路径要和接口文档一致:文档写 /api/banner/list,你就不能写成 /api/banners。前端照着文档写,后端照着文档做,两边才接得上。
避坑两个视图函数起同一个名字,Flask 会在启动时报 AssertionError: View function mapping is overwriting an existing endpoint function: xxx。原因是函数名默认就是「端点名」(endpoint),要求全局唯一。别偷懒复制粘贴后忘了改函数名——中文项目里很容易出现两个 def detail():。真需要同名时,用 @app.route("/api/a", endpoint="article_a") 显式指定端点名。

变量规则:把 URL 里的数据取出来

int / string / float / path
变量规则:把 URL 里的数据取出来 URL 模板 /article/<int:id> 转换器(决定类型) int → 整数 string → 字符串(默认) float/path 视图函数 detail(id) 请求 /article/7 → <int:id> 匹配到 7 → detail(id=7) 拿到的 id 就是整数 7 如果写 <id>(不指定转换器),默认是 string,拿到的是字符串 "7"

URL 里的变量用 <int:id> 这样的写法捕获,尖括号里的转换器决定参数类型。请求 /article/7 时,<int:id> 会把 7 转成整数传给 detail(id=7)。

先讲为什么「古典文学网」有 6 篇作品,详情页自然是 /api/article/detail/1、/api/article/detail/3……如果为每一篇作品写一个路由,来第七篇就得再加一个函数,这是不可维护的。
正确的想法是:URL 的形状是一样的,只有中间那个数字在变。把这个「会变的部分」用尖括号挖出来当参数,一条规则就能接住所有作品——这就是变量规则。
动手做在路径里写 <类型:变量名>,Flask 就会把命中这一段的值按类型转换好,作为参数传给视图函数。注意视图函数的形参名必须和尖括号里的名字一模一样。
变量规则.pypython
# ① int:只匹配整数(最常用),URL 里出现非数字直接 404
@app.route("/api/article/detail/<int:aid>")
def article_detail(aid):
    # 进来的 aid 已经是 int,可以直接参与运算,不用 int() 转换
    return jsonify({"code": 200, "msg": "success", "data": {"id": aid}})


# ② string:匹配一段不含斜杠的文本(默认类型,不写也行)
@app.route("/api/user/<string:username>")
def user_info(username):
    return jsonify({"code": 200, "msg": "success", "data": {"username": username}})


# ③ float:匹配小数
@app.route("/api/rank/<float:score>")
def rank(score):
    return jsonify({"code": 200, "msg": "success", "data": {"score": score}})


# ④ path:允许匹配带斜杠的完整路径,适合文件下载
@app.route("/api/download/<path:filepath>")
def download(filepath):
    return jsonify({"code": 200, "msg": "success", "data": {"filepath": filepath}})
运行输出Python 运行结果
运行结果: $ flask --app app routes Endpoint Methods Rule ----------------- ------- -------------------------------- static GET /static/<path:filename> index GET / banner_list GET /api/banner/list category_list GET /api/category/list article_detail GET /api/article/detail/<int:aid> user_info GET /api/user/<string:username> rank GET /api/rank/<float:score> download GET /api/download/<path:filepath> $ curl -s http://127.0.0.1:5000/api/article/detail/3 | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "id": 3 } } $ curl -s http://127.0.0.1:5000/api/rank/9.5 {"code":200,"msg":"success","data":{"score":9.5}} $ curl -s http://127.0.0.1:5000/api/download/upload/avatar/3.png {"code":200,"msg":"success","data":{"filepath":"upload/avatar/3.png"}} $ curl -o /dev/null -s -w "%{http_code}\n" http://127.0.0.1:5000/api/article/detail/abc 404
讲透
  • 四种转换器:string(默认,不含 /)、int(整数)、float(小数)、path(含 /)。uuid 和 any 也支持,本项目用不到。
  • 类型转换是白送的校验:规则写了 <int:aid>,/api/article/detail/abc 连你的函数都进不去,直接 404。这比在函数里写 if not aid.isdigit() 省事得多。
  • 转换器写错会导致匹配不到:把 <int:aid> 写成 <string:aid>,虽然也能匹配到 3,但你拿到的是字符串 "3",后面查数据库、算分页都可能出问题——类型对了,麻烦少一半。
  • 一个 URL 可以有多个变量:/api/comment/list/<int:article_id>/<int:page>。不过按接口文档的约定,分页这类参数走查询字符串更好(第 3 章讲 request.args)。
  • 命名要和行为一致:详情接口用 aid、cid、mid 这类「表名首字母 + id」的写法,看到 aid 就知道是作品 id,团队里不用猜。
避坑① 形参名对不上:规则写 <int:aid>,函数却写 def article_detail(id):,启动时直接报 TypeError: article_detail() missing 1 required positional argument,或者 got an unexpected keyword argument 'aid'。名字必须一模一样。② 别把变量名叫 id——它虽然能用,但会遮蔽 Python 内置的 id() 函数,代码检查工具也会一直提示你,用 aid 更省心。

同一路径,不同 HTTP 方法

GET 查、POST 增、PUT 改、DELETE 删
同一路径 + 不同方法 = 不同动作 URL: /article 资源地址固定 GET → 列表 POST → 新建 PUT → 更新 DELETE → 删除 为什么? URL 说「对哪份资源」 方法说「干什么」 这就是 RESTful

同一个资源地址 /article,配不同的 HTTP 方法(GET/POST/PUT/DELETE)就代表「查/增/改/删」四种动作——这正是 RESTful 风格的核心思路。

先讲为什么接口文档里写着 GET /api/user/profile 是「获取个人信息」,PUT /api/user/profile 是「修改个人信息」——路径完全一样,靠 HTTP 方法区分动作。
这就像同一个快递柜编号,取件和寄件是两件事。好处是路径保持简洁(不用写成 /getProfile 和 /updateProfile),语义也更清楚:GET 只读、不会改数据,POST/PUT/DELETE 才会。
动手做用 methods 参数声明这个视图愿意接哪些方法。默认只接 GET(还有自动支持的 HEAD、OPTIONS),所以 POST 接口必须显式写出来。
methods.pypython
from flask import Flask, jsonify, request

app = Flask(__name__)
app.json.ensure_ascii = False


# 写法一:一个接口只干一件事,最清晰 —— 推荐
@app.route("/api/comment/list", methods=["GET"])
def comment_list():
    return jsonify({"code": 200, "msg": "success",
                    "data": {"list": [], "total": 0, "page": 1, "page_size": 10}})


@app.route("/api/comment/add", methods=["POST"])
def comment_add():
    data = request.get_json()
    return jsonify({"code": 200, "msg": "发表成功", "data": {"comment_id": 9}})


# 写法二:同一个路径、同一个函数处理两种方法,用 request.method 分流
@app.route("/api/user/profile", methods=["GET", "PUT"])
def user_profile():
    if request.method == "GET":
        return jsonify({"code": 200, "msg": "success",
                        "data": {"id": 3, "username": "zhangsan", "nickname": "张三"}})
    # 走到这里就是 PUT:修改资料
    return jsonify({"code": 200, "msg": "修改成功", "data": None})
运行输出Python 运行结果
运行结果: # 用错方法:GET 去访问只收 POST 的接口 → 405 $ curl -i http://127.0.0.1:5000/api/comment/add HTTP/1.1 405 METHOD NOT ALLOWED Allow: POST, OPTIONS Content-Type: text/html; charset=utf-8 # 用对方法:POST → 正常 $ curl -s -X POST http://127.0.0.1:5000/api/comment/add \ -H "Content-Type: application/json" \ -d '{"article_id": 1, "content": "好诗!", "parent_id": null}' {"code":200,"msg":"发表成功","data":{"comment_id":9}} # 同一路径,两种方法,两种结果 $ curl -s http://127.0.0.1:5000/api/user/profile | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "id": 3, "username": "zhangsan", "nickname": "张三" } } $ curl -s -X PUT http://127.0.0.1:5000/api/user/profile \ -H "Content-Type: application/json" -d '{"nickname": "张三丰"}' {"code":200,"msg":"修改成功","data":null}
讲透
  • 五种常见方法:GET 查、POST 增、PUT 改(整体)、DELETE 删。另外 OPTIONS 由 Flask 自动处理,用来告诉浏览器「这个接口允许哪些方法」。
  • 405 和 404 不是一回事:404 是「没有这条路」,405 是「路找到了,但不许你这种走法」。看到 405,去检查 methods 参数和你的请求方法,别看前面的路径。
  • 能用两个函数就别用一个函数分流:写法二只在「读和写逻辑高度重合」时才划算;多数情况(如登录、点赞)拆成两个函数更容易读、也更好单独加鉴权装饰器。
  • 方法语义要尊重:GET 应该「只读」——不然浏览器预加载、爬虫抓页面时都可能顺手改了你的数据。这是很真的坑,不是洁癖。
  • 跨域会先来一个 OPTIONS 预检:前端 Vue 跑在 5173、后端在 5000,属于跨域。浏览器会先发 OPTIONS 问一句「能跨域吗」,这个要靠 Flask-Cors 处理(第 5 章)。
避坑① 忘了写 methods:想让前端 POST /api/comment/add,却只写了 @app.route("/api/comment/add"),结果收到 405,而你会以为是前端没发出去。② 用 GET 传敏感数据:把 token 拼在 URL 上(/api/user/profile?token=xxx)——URL 会被服务器日志、浏览器历史、代理原样记下来。接口文档明确要求鉴权信息走请求头 Authorization: Bearer <token>。

url_for:反着推地址

别在代码里写死路径
url_for:反着推地址,别写死路径 代码里写 url_for('detail', id=1) 查路由表 按端点名反查规则 生成地址 /article/1 好处:以后把路由改成 /works/<int:id>,所有用 url_for 生成的链接自动跟着变, 不用满项目去搜 "/article/" 手工替换。

url_for 按端点名反向生成真实地址。路由规则改了,用 url_for 生成的链接会自动跟着更新,避免满项目手改字符串。

先讲为什么今天你把详情页接口写成 /api/article/detail/3,代码里到处拼这个字符串。下周你决定加版本号,改成 /api/v2/article/detail/3——于是你得翻遍所有文件改字符串,漏掉一处就是线上 bug。
「路径」这件事只应该有一个地方定义,也就是 @app.route 那一行。url_for() 就是反过来用的:你给我视图函数的名字,我按路由表算出现成的 URL。
动手做在列表接口里,把每条作品的详情地址用 url_for 算出来一起返回。第一个参数是端点名(默认就是函数名),不是路径;路由里的变量用关键字参数传进去。
url_for.pypython
from flask import Flask, jsonify, url_for

app = Flask(__name__)
app.json.ensure_ascii = False


@app.route("/api/article/detail/<int:aid>")
def article_detail(aid):
    return jsonify({"code": 200, "msg": "success", "data": {"id": aid}})


@app.route("/api/article/list")
def article_list():
    rows = [
        {"id": 1, "title": "静夜思", "author": "李白"},
        {"id": 3, "title": "念奴娇·赤壁怀古", "author": "苏轼"},
    ]
    for row in rows:
        # 不用手拼 "/api/article/detail/" + str(row["id"])
        row["detail_url"] = url_for("article_detail", aid=row["id"])

    return jsonify({
        "code": 200,
        "msg": "success",
        "data": {"list": rows, "total": 6, "page": 1, "page_size": 10},
    })


@app.route("/")
def index():
    # 静态文件也能生成地址:static 是 Flask 自带的端点
    avatar = url_for("static", filename="upload/avatar/3.png")
    # 额外参数会变成查询字符串:/api/article/list?page=2
    more = url_for("article_list", page=2)
    return jsonify({"code": 200, "msg": "success",
                    "data": {"avatar": avatar, "more": more}})
运行输出Python 运行结果
运行结果: $ curl -s http://127.0.0.1:5000/api/article/list | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "list": [ { "id": 1, "title": "静夜思", "author": "李白", "detail_url": "/api/article/detail/1" }, { "id": 3, "title": "念奴娇·赤壁怀古", "author": "苏轼", "detail_url": "/api/article/detail/3" } ], "total": 6, "page": 1, "page_size": 10 } } $ curl -s http://127.0.0.1:5000/ | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "avatar": "/static/upload/avatar/3.png", "more": "/api/article/list?page=2" } }
讲透
  • 第一个参数是端点名,不是 URL。端点名默认取函数名(article_detail),所以改名函数时,别忘了同步改 url_for 里的字符串。写错会报 BuildError: Could not build url for endpoint 'xxx'。
  • 路由变量按关键字传:规则是 <int:aid>,就写 url_for("article_detail", aid=3)。少传或名字不对都会报 BuildError。
  • 多余的参数自动变成查询字符串:url_for("article_list", page=2, keyword="月") 会得到 /api/article/list?page=2&keyword=%E6%9C%88——中文会被正确 URL 编码,这是手拼字符串最容易出错的地方。
  • 那前端要不要用 url_for?不要。接口路径对前端来说是契约,前端照接口文档写死 /api/article/list 是正常的(真实项目里还会抽到统一的 request.js 里配 BaseURL)。url_for 管的是后端内部:后端自己生成地址(拼接详情链接、跳转、静态文件路径、邮件里的链接)时,一律用它。
  • 调试小技巧:flask --app app routes 会打印完整路由表,端点名一栏就是 url_for 要用的名字。

尾部斜杠的坑

/article 和 /article/ 不是一回事
先讲为什么在 URL 里,末尾那个斜杠是有意义的字符,不是装饰。/article 和 /article/ 在 Flask 看来是两条不同的规则,行为也不同。很多人写完接口拿去 curl,明明路径看着一模一样却报 404 或 308,多半就是它。花三分钟搞清楚,能省你以后很多次抓头。
动手做分两种情形各试一次。情形 A:规则末尾带斜杠。
情形A_带斜杠.pypython
# 规则带尾部斜杠:访问不带斜杠的地址时,Flask 会自动 308 重定向过去
@app.route("/article/")
def article_page():
    return "带斜杠的规则"
运行输出Python 运行结果
运行结果: $ curl -i http://127.0.0.1:5000/article HTTP/1.1 308 PERMANENT REDIRECT Location: http://127.0.0.1:5000/article/ Content-Type: text/html; charset=utf-8 Content-Length: 233 $ curl -sL http://127.0.0.1:5000/article 带斜杠的规则 $ curl -s http://127.0.0.1:5000/article/ 带斜杠的规则
动手做情形 B:规则末尾不带斜杠。这时反方向访问会直接 404,不会帮你重定向。
情形B_不带斜杠.pypython
# 规则不带尾部斜杠:访问带斜杠的地址会直接 404,没有任何提示
@app.route("/article")
def article_page():
    return "不带斜杠的规则"
运行输出Python 运行结果
运行结果: $ curl -i http://127.0.0.1:5000/article/ HTTP/1.1 404 NOT FOUND Content-Type: text/html; charset=utf-8 Content-Length: 207 $ curl -s http://127.0.0.1:5000/article 不带斜杠的规则
讲透:原理和规矩
  • 默认规则(strict_slashes=True):带斜杠的规则会收留不带斜杠的访问(308 转过去),不带斜杠的规则拒绝带斜杠的访问(404)。记成一句话就够了——斜杠只能多、不能少。
  • 为什么带斜杠的规则要重定向?因为 /article/ 的语义是「一个目录」,目录可以容纳子路径;/article 是「一个具体资源」。Flask 顺着这个语义做了一致性处理。
  • 可以用 strict_slashes=False 关掉严格模式:@app.route("/article", strict_slashes=False),这样带不带斜杠都能访问,不再 404。
  • API 接口建议统一不带尾部斜杠:接口文档里写的全是 /api/article/list、/api/user/login,那就别在代码里写成 /api/article/list/。前后端按同一份文档写,这类问题根本不会发生。
  • 308 不要当 301 看:308 是永久重定向,浏览器会记住。开发阶段如果改了路径,可能遇到浏览器还在跳老地址的情况,清一下缓存或换个无痕窗口。
避坑最坑的组合是前端用 axios 发 POST,后端规则带了尾部斜杠:跨域情况下,308 重定向会带着请求体跳到新地址,有些客户端不会重发 POST 体,于是你的接口「明明收到请求,参数却是空的」,而且完全看不到 404。解决办法很土但有效——两边写完全一样的路径,别让浏览器有机会重定向。

实战:作品详情页的路由

把本章串起来
动手做按接口文档,作品详情是 GET /api/article/detail/<id>。本章先用手写数据把路由跑通,第 6 章把它换成真正查 MySQL(还会顺便实现「每次访问浏览量 +1」)。
app.py · 作品详情python
@app.route("/api/article/detail/<int:aid>", methods=["GET"])
def article_detail(aid):
    """作品详情:先用手写数据,第 6 章换成查 article 表"""
    articles = {
        1: {"id": 1, "title": "静夜思", "author": "李白", "dynasty": "唐",
            "category_name": "唐诗", "views": 329, "likes": 42},
        3: {"id": 3, "title": "念奴娇·赤壁怀古", "author": "苏轼", "dynasty": "宋",
            "category_name": "宋词", "views": 412, "likes": 58},
    }
    article = articles.get(aid)
    if article is None:
        # 业务失败也返回 HTTP 200,成败看 code —— 按接口文档的统一约定
        return jsonify({"code": 404, "msg": "作品不存在", "data": None})

    return jsonify({"code": 200, "msg": "success", "data": article})
运行输出Python 运行结果
运行结果: $ flask --app app routes Endpoint Methods Rule ----------------- ------- -------------------------------- static GET /static/<path:filename> index GET / banner_list GET /api/banner/list category_list GET /api/category/list article_list GET /api/article/list article_detail GET /api/article/detail/<int:aid> $ curl -s http://127.0.0.1:5000/api/article/detail/3 | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "id": 3, "title": "念奴娇·赤壁怀古", "author": "苏轼", "dynasty": "宋", "category_name": "宋词", "views": 412, "likes": 58 } } $ curl -s http://127.0.0.1:5000/api/article/detail/999 | python -m json.tool --indent 2 { "code": 404, "msg": "作品不存在", "data": null }
讲透:写一个路由的固定套路
  • ① 查文档定路径和方法:GET /api/article/detail/<id>。
  • ② 想清楚哪一段是变量:id 是整数,用 <int:aid>,让 Flask 顺手把类型校验也做了。
  • ③ 函数名取和业务对应的名字:article_detail,这个名字同时就是 url_for 要用的端点名。
  • ④ 先校验再干活:查不到就返回 404 的业务码;不要在这里抛异常(那样会变成 500)。
  • ⑤ 返回值统一包装成 {code, msg, data},前端只写一套判断逻辑。
  • 这一套流程对每一个接口都一样,你以后写第十个接口时,只是在重复这五步。
动手练习
1写出书评列表接口 GET /api/comment/list,接收 article_id 作为变量放在路径里(形如 /api/comment/list/<int:article_id>),返回 {code, msg, data},data 里包一个空的 list。
2写一个路径为 /api/favorite/toggle 的路由,同时接受 GET 和 POST:GET 时返回「请用 POST 提交」,POST 时返回 {"code": 200, "msg": "操作成功", "data": {"favorited": true}}。然后想清楚:如果只写 @app.route("/api/favorite/toggle"),POST 访问会得到哪个状态码?
3迁移练习:「红影赏析」的电影详情接口是 GET /api/movie/detail/<int:mid>。请写出这个路由,并在列表接口里用 url_for 反推出每条电影的详情地址。
参考答案
练习答案.pypython
from flask import Flask, jsonify, url_for, request

app = Flask(__name__)
app.json.ensure_ascii = False


# 第 1 题
@app.route("/api/comment/list/<int:article_id>", methods=["GET"])
def comment_list(article_id):
    return jsonify({
        "code": 200,
        "msg": "success",
        "data": {"list": [], "total": 0, "page": 1, "page_size": 10},
    })


# 第 2 题
@app.route("/api/favorite/toggle", methods=["GET", "POST"])
def favorite_toggle():
    if request.method == "GET":
        return jsonify({"code": 400, "msg": "请用 POST 提交", "data": None})
    return jsonify({"code": 200, "msg": "操作成功", "data": {"favorited": True}})


# 第 3 题(红影赏析)
@app.route("/api/movie/detail/<int:mid>", methods=["GET"])
def movie_detail(mid):
    return jsonify({
        "code": 200,
        "msg": "success",
        "data": {"id": mid, "title": "流浪地球", "genre": "科幻", "rating": 8.4},
    })


@app.route("/api/movie/list")
def movie_list():
    rows = [
        {"id": 1, "title": "流浪地球"},
        {"id": 2, "title": "星际穿越"},
    ]
    for row in rows:
        row["detail_url"] = url_for("movie_detail", mid=row["id"])
    return jsonify({"code": 200, "msg": "success", "data": {"list": rows}})
运行输出Python 运行结果
运行结果: $ curl -s http://127.0.0.1:5000/api/comment/list/1 | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "list": [], "total": 0, "page": 1, "page_size": 10 } } $ curl -s -X POST http://127.0.0.1:5000/api/favorite/toggle {"code":200,"msg":"操作成功","data":{"favorited":true}} $ curl -s http://127.0.0.1:5000/api/movie/list | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "list": [ { "id": 1, "title": "流浪地球", "detail_url": "/api/movie/detail/1" }, { "id": 2, "title": "星际穿越", "detail_url": "/api/movie/detail/2" } ] } } $ curl -o /dev/null -s -w "%{http_code}\n" http://127.0.0.1:5000/api/article/list/ 404

第 2 题的答案:只写 @app.route("/api/favorite/toggle") 时,这个路由只收 GET,POST 访问会得到 405 METHOD NOT ALLOWED(不是 404)。第 3 题的关键:路由的写法、变量规则、url_for 的用法全都一样,变的只是路径里的业务名。

本章小结

📝 本节自测

做完点「检查本节」看解析与得分;做错可重做。