Skip to content

组织路由 ​

文章服务逐渐有了列表、详情、创建和后台入口。此时仅靠堆放 Route 很难看清请求会落到哪里。本专题围绕“同一组文章接口如何分发和组织”学习路由规则。

路由要同时考虑路径、请求方法和注册顺序。命名路由解决 URL 反向生成;Mount 用于组织一组子路由。示例代码见下文。

启动 05_article_routes.py 后,可运行 routing_demo.py 查看状态码与响应体。

固定路径与处理函数 ​

Route 把路径与处理函数关联起来。先用固定路径建立最简单的分发关系,再增加文章列表。

先看最基础的路由写法。

示例文件:01_basic_routes.py。运行:uvicorn 01_basic_routes:app --reload。

py
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route


async def homepage(request):
    return PlainTextResponse("首页")


async def article_list(request):
    return PlainTextResponse("文章列表")


routes = [
    Route("/", homepage),
    Route("/articles", article_list),
]

app = Starlette(routes=routes)

if __name__ == '__main__':
    import uvicorn

    uvicorn.run("01_basic_routes:app", host='127.0.0.1', port=8000, reload=True)

这段代码只做最基础的分发:

text
/         → homepage
/articles → article_list

也就是:

text
不同路径
对应不同处理函数

这里只需要先记住:

text
Route 负责描述“哪个路径交给谁处理”
routes 列表负责把多条路由放在一起
Starlette(routes=routes) 负责把它们注册到应用中

路径参数与请求方法 ​

{article_id:int} 匹配并转换编号,methods 限制允许的 HTTP 方法。固定路径应放在可能与它冲突的参数路径前面。

接下来增加两个常见能力:

  1. 路径参数
  2. 请求方法限制

示例文件:02_path_and_methods.py。运行:uvicorn 02_path_and_methods:app --reload。

py
from starlette.applications import Starlette
from starlette.responses import JSONResponse, PlainTextResponse
from starlette.routing import Route


async def article_list(request):
    return PlainTextResponse("GET:文章列表")


async def article_detail(request):
    article_id = request.path_params["article_id"]
    return JSONResponse({"message": "文章详情", "article_id": article_id})


async def article_create(request):
    return PlainTextResponse("POST:模拟新增文章")


routes = [
    Route("/articles", article_list, methods=["GET"]),
    Route("/articles/{article_id:int}", article_detail, methods=["GET"]),
    Route("/articles", article_create, methods=["POST"]),
]

app = Starlette(routes=routes)

if __name__ == '__main__':
    import uvicorn

    uvicorn.run("02_path_and_methods:app", host='127.0.0.1', port=8000, reload=True)

传递路径参数 ​

这一行:

python
Route("/articles/{article_id:int}", article_detail)

表示访问这类地址时:

text
/articles/1
/articles/2
/articles/100

都交给 article_detail,并把数字部分作为 article_id 传进去。

限制请求方法 ​

下面两行:

python
Route("/articles", article_list, methods=["GET"])
Route("/articles", article_create, methods=["POST"])

表示:

text
GET  /articles  → article_list
POST /articles  → article_create

路径一样,但方法不同,处理函数也不同。

重点记三件事:

  1. 路径参数可以写在路径里。
  2. 路径参数可以在 request.path_params 中读取。
  3. 同一路径可以按请求方法分流到不同处理函数。

路径匹配与 HTTP 请求方法分发状态机

生成命名路由 URL ​

给路由命名后,request.url_for() 可以按名称生成 URL。修改路径时,使用它的跳转代码不必拼接字符串。

接下来学习给路由命名,以及通过名字生成 URL。

示例文件:03_named_routes.py。运行:uvicorn 03_named_routes:app --reload。

py
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse, RedirectResponse
from starlette.routing import Route


async def homepage(request):
    return PlainTextResponse("首页")


async def article_list(request):
    return PlainTextResponse("文章列表页面")


async def old_articles(request):
    url = request.url_for("article_list")
    return RedirectResponse(url)


routes = [
    Route("/", homepage, methods=["GET"], name="homepage"),
    Route("/articles", article_list, methods=["GET"], name="article_list"),
    Route("/old-articles", old_articles, methods=["GET"], name="old_articles"),
]

app = Starlette(routes=routes)

if __name__ == '__main__':
    import uvicorn

    uvicorn.run("03_named_routes:app", host='127.0.0.1', port=8000, reload=True)

这里的核心是:

python
request.url_for("article_list")

它表示:

text
不要手写 /articles
而是通过路由名字生成对应地址

然后:

python
RedirectResponse(new_url)

把旧地址重定向到新地址。

使用 url_for 的注意事项 ​

如果使用 url_for(),要保证:

text
目标路由已经设置了 name
名字拼写正确

正向请求路由与 url_for 反向生成机制

用 Mount 分组后台路由 ​

后台或独立功能可以放入 Mount 下。子路由仍按原有规则匹配,只是共享路径前缀。

现在看如何把一组相关路由统一放到某个前缀下。

示例文件:04_admin_mount.py。运行:uvicorn 04_admin_mount:app --reload。

py
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Mount, Route


async def admin_homepage(request):
    return PlainTextResponse("后台首页")


async def admin_articles(request):
    return PlainTextResponse("后台文章管理")


admin_routes = [
    Route("/", admin_homepage, methods=["GET"], name="homepage"),
    Route("/articles", admin_articles, methods=["GET"], name="articles"),
]

routes = [
    Mount("/admin", routes=admin_routes, name="admin"),
]

app = Starlette(routes=routes)
if __name__ == '__main__':
    import uvicorn

    uvicorn.run("04_admin_mount:app", host='127.0.0.1', port=8000, reload=True)

这段代码表示:

text
/admin/          → admin_home
/admin/articles  → admin_articles

也就是先定义一组后台路由,再统一挂到 /admin 下。

Mount 的适用场景 ​

Mount 适合这种情况:

text
一组路由有共同前缀
例如 /admin、/api、/static

这样能让路由结构更清晰。

Mount 子路由挂载与前缀委托机制

路由匹配顺序 ​

路由是按顺序匹配的。

这意味着更具体的路由,通常要放在更前面。

例如下面这种顺序就有风险:

python
routes = [
    Route("/articles/{article_id}", article_detail),
    Route("/articles/new", article_new),
]

如果没有类型限制,访问 /articles/new 时,new 可能会先被当成 article_id。

更稳妥的写法通常是:

python
routes = [
    Route("/articles/new", article_new),
    Route("/articles/{article_id:int}", article_detail),
]

可以先记住这个经验:

text
更具体的路由放前面
更通用的动态路由放后面

路由匹配顺序与参数遮蔽陷阱 (Shadowing)

文章服务路由表 ​

最终路由表把文章与后台入口放在同一个应用中。

目标包括:

text
/                    首页
/articles            文章列表
/articles/new        新建文章说明
/articles/{article_id:int} 文章详情
POST /articles       模拟新增文章
/old-articles        旧地址跳转
/admin/              后台首页
/admin/articles      后台文章管理

示例文件:05_article_routes.py。运行:uvicorn 05_article_routes:app --reload。

py
from starlette.applications import Starlette
from starlette.responses import JSONResponse, PlainTextResponse, RedirectResponse
from starlette.routing import Mount, Route

ARTICLES = [
    {"id": 1, "title": "认识 Starlette 应用对象"},
    {"id": 2, "title": "学习 Request 请求对象"},
    {"id": 3, "title": "学习 Response 响应对象"},
    {"id": 4, "title": "学习 Starlette 路由系统"},
]


async def homepage(request):
    return PlainTextResponse("首页")


async def article_list(request):
    return PlainTextResponse("文章列表")


async def article_detail(request):
    article_id = request.path_params["article_id"]
    return JSONResponse({"message": "文章详情", "article_id": article_id})


async def article_new(request):
    return PlainTextResponse("新建文章说明页面")


async def article_create(request):
    return PlainTextResponse("POST:模拟新增文章")


async def old_articles(request):
    url = request.url_for("article_list")
    return RedirectResponse(url)


async def admin_homepage(request):
    return PlainTextResponse("后台首页")


async def admin_articles(request):
    return PlainTextResponse("后台文章管理")


article_routes = [
    Route("/articles", article_list, methods=["GET"], name="article_list"),
    Route("/articles", article_create, methods=["POST"], name="article_create"),
    Route("/articles/new", article_new, methods=["GET"], name="article_new"),
    Route("/articles/{article_id:int}", article_detail, methods=["GET"], name="article_detail"),
]

admin_routes = [
    Route("/", admin_homepage, methods=["GET"], name="homepage"),
    Route("/articles", admin_articles, methods=["GET"], name="articles"),
]

routes = [
    Route("/", homepage, methods=["GET"], name="homepage"),
    *article_routes,
    Route("/old-articles", old_articles, methods=["GET"], name="old_articles"),
    Mount("/admin", routes=admin_routes, name="admin"),
]

app = Starlette(routes=routes)

if __name__ == '__main__':
    import uvicorn

    uvicorn.run("05_article_routes:app", host='127.0.0.1', port=8000, reload=True)

这个案例把本章几个核心点放到了一起:

text
普通路径
路径参数
请求方法限制
路由命名
URL 反向生成
重定向
Mount 前缀组织

它的重点不是业务复杂,而是帮助你建立一张清晰的路由地图。

案例范围 ​

这个案例已经足够说明:

text
不同路径如何分发
同一路径不同方法如何分发
动态路径如何处理
相关路由如何按前缀组织

但它还没有进入更复杂的项目拆分。 本章先把路由系统本身讲清楚,复杂目录结构以后再说。

文章服务完整路由树状全景图

案例源码 ​

py
import requests


BASE_URL = "http://127.0.0.1:8000"


def test_article_paths():
    for path in ("/articles", "/articles/new", "/articles/1"):
        response = requests.get(f"{BASE_URL}{path}", timeout=5)
        print(path, response.status_code, response.text)


def test_method_dispatch():
    response = requests.post(f"{BASE_URL}/articles", timeout=5)
    print("POST /articles", response.status_code, response.text)


def test_named_redirect():
    response = requests.get(
        f"{BASE_URL}/old-articles", allow_redirects=False, timeout=5
    )
    print("GET /old-articles", response.status_code)
    print("Location:", response.headers.get("location"))


def test_mounted_routes():
    response = requests.get(f"{BASE_URL}/admin/articles", timeout=5)
    print("GET /admin/articles", response.status_code, response.text)


if __name__ == "__main__":
    test_article_paths()
    test_method_dispatch()
    test_named_redirect()
    test_mounted_routes()

是否可以直接帮我修改当前项目的配置文件进行关闭

将默认 ai 的登录也关闭

为何 vscode 里面无法使用中文输入法

是不是系统的原因,按切换输入法没有反应

输入法是 fcitx5 的雾凇输入法

应该是连接问题,怎么解决

问题还是没有解决

好像还与 vs code 的快捷键冲突了