Skip to content

第 18 章 · 模板渲染与静态文件

本章目标:掌握 Jinja2 模板渲染、静态文件托管,以及"模板 + API"混合架构与 SPA 前端托管方案。

18.1 什么时候需要服务端模板

前面章节的 FastAPI 都是纯 JSON API。但很多真实项目需要"混合架构":

  • 管理后台、报表页、邮件模板渲染等由后端直接输出 HTML;
  • 核心业务同时暴露 JSON API 给前端框架或第三方调用。

FastAPI(借助 Starlette)对模板引擎零侵入:任何模板引擎都能用,官方最常用的是 Jinja2(Flask 同款)。

安装依赖:

bash
uv add jinja2
# 或 pip install jinja2

18.2 Jinja2Templates 与 TemplateResponse

使用模板的固定四步:

  1. 导入并创建 Jinja2Templates 对象(可全局复用);
  2. 在路径函数中声明 Request 参数;
  3. templates.TemplateResponse(...) 渲染;
  4. 模板里通过上下文变量取值。
python
# main.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()

# 创建一次,全局复用;directory 指向模板目录
templates = Jinja2Templates(directory="templates")


@app.get("/items/{id}", response_class=HTMLResponse)  # 告知文档 UI 返回的是 HTML
def read_item(request: Request, id: int):
    # 新版签名:request 是第一个参数,上下文单独传
    return templates.TemplateResponse(
        request=request,
        name="item.html",           # 相对 templates 目录
        context={"id": id},          # 传给模板的键值对
    )

版本差异

FastAPI 0.108.0 / Starlette 0.29.0 之前,name 是第一个参数,且 request 要放进 context 字典里。网上老教程写法是 TemplateResponse(name="item.html", context={"request": request, ...}),新项目请统一用上面 request 在前的新签名,否则会收到弃用警告。

模板文件 templates/item.html

html
<html>
  <head>
    <title>Item 详情</title>
  </head>
  <body>
    <h1>Item ID: {{ id }}</h1>  <!-- {{ }} 输出上下文变量 -->
    <ul>
      {% for tag in ["a", "b"] %}   <!-- {% %} 是 Jinja2 语句语法 -->
        <li>{{ tag }}</li>
      {% endfor %}
    </ul>
  </body>
</html>

访问 /items/42 会渲染出 Item ID: 42

18.3 模板里的 url_for

模板中可以直接调用 url_for,参数与路径操作函数完全一致,生成对应路由的真实 URL——这样改路由前缀时模板链接不用跟着改:

html
<body>
  <a href="{{ url_for('read_item', id=id) }}">查看 {{ id }}</a>
  <!-- id=42 时渲染为 <a href="/items/42">查看 42</a> -->
</body>

url_for 第一个参数是路径操作函数名(不是路径),后续是它的参数。这是模板与路由解耦的关键手段。

18.4 StaticFiles:挂载静态目录

CSS/JS/图片等静态资源用 StaticFiles 托管,通过 mount 挂到某个路径前缀:

python
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()

# 挂载一个完全独立的"子应用"处理 /static 开头的所有请求
app.mount("/static", StaticFiles(directory="static"), name="static")

三个参数的含义:

  • "/static":挂载的 URL 前缀,所有 /static/... 请求都交给它;
  • directory="static":磁盘上的资源目录;
  • name="static":内部名称,供 url_for 使用。

"挂载(Mount)"与 include_router 的区别

mount 加入的是一个完全独立的应用:它有自己的路由体系,不会出现在主应用的 OpenAPI 文档里;APIRouter 则是主应用路由的一部分。静态文件、整个前端应用适合 mount,API 分组适合 router。

配合 url_for 引用静态资源:

html
<head>
  <link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">
</head>
<!-- 渲染为 <link href="/static/styles.css" rel="stylesheet"> -->

18.5 托管 SPA 与混合架构

现代前端(Vue/React 打包产物)是单页应用:所有未匹配路径都应回退到 index.html,由前端路由接管。新版 FastAPI 提供了专门方法:

python
# 方式一(推荐):app.frontend() 内部使用 StaticFiles,
# 并额外处理了客户端路由(history 模式刷新 404 问题)
from fastapi import FastAPI

app = FastAPI()

# 前端构建产物放在 frontend/dist,API 与前端同域部署
app.frontend(directory="frontend/dist")

经典的手写方案是"挂载 + catch-all 回退":

python
from fastapi.responses import FileResponse

# 先声明所有 API 路由,最后兜底
@app.get("/{full_path:path}")
def spa_fallback(full_path: str):
    # 命中真实静态文件则返回文件,否则回退 index.html 交给前端路由
    return FileResponse("frontend/dist/index.html")

顺序很重要

catch-all 路由必须放在所有 API 路由之后注册,否则会吞掉 /docs/openapi.json 和所有 API 请求。这也是"模板 + API"混合部署时最常见的坑。

混合架构的典型分层:

  • /api/* → JSON API(本章之前学的全部内容);
  • /static/* → 静态资源;
  • / → Jinja2 渲染的页面或 SPA 的 index.html

18.6 本章小结

  • Jinja2Templates(directory=...) 创建一次全局复用;路径函数需声明 Request 参数;
  • 新版签名是 TemplateResponse(request=request, name=..., context=...),旧写法(name 在前、request 塞进 context)已过时;
  • url_for 在模板中按函数名生成链接,实现模板与路由解耦;
  • app.mount() 挂载独立子应用托管静态文件,不进 OpenAPI 文档;
  • SPA 托管优先用 app.frontend(),手写 catch-all 时务必放在最后。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 当前版本 FastAPI 中 TemplateResponse 的推荐调用方式是?

2. 关于 app.mount("/static", StaticFiles(directory="static"), name="static"),说法错误的是?

3. 模板中 {{ url_for("read_item", id=3) }} 的作用是?

4. 用 catch-all 路由(/{full_path:path})为 SPA 提供 history 回退时,最需要注意什么?

🛠️ 动手实践

  1. 为一个"待办事项"应用同时实现 POST /api/todos(JSON API)和 GET /todos(Jinja2 渲染的 HTML 列表页),列表页展示全部待办。
  2. 给第 1 题的页面加一个外部 CSS(通过 StaticFiles 挂载并在模板中用 url_for 引用),把表格隔行变色。
  3. 把一个 Vue/React 构建产物(或手写的多页面静态站)用 app.frontend() 或 catch-all 方式挂到 FastAPI 上,验证刷新子路径不 404。

下一章我们把单文件应用拆成规范的多模块工程:第 19 章 · 大型项目结构拆分