第 18 章 · 模板渲染与静态文件
本章目标:掌握 Jinja2 模板渲染、静态文件托管,以及"模板 + API"混合架构与 SPA 前端托管方案。
18.1 什么时候需要服务端模板
前面章节的 FastAPI 都是纯 JSON API。但很多真实项目需要"混合架构":
- 管理后台、报表页、邮件模板渲染等由后端直接输出 HTML;
- 核心业务同时暴露 JSON API 给前端框架或第三方调用。
FastAPI(借助 Starlette)对模板引擎零侵入:任何模板引擎都能用,官方最常用的是 Jinja2(Flask 同款)。
安装依赖:
uv add jinja2
# 或 pip install jinja218.2 Jinja2Templates 与 TemplateResponse
使用模板的固定四步:
- 导入并创建
Jinja2Templates对象(可全局复用); - 在路径函数中声明
Request参数; - 用
templates.TemplateResponse(...)渲染; - 模板里通过上下文变量取值。
# 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>
<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——这样改路由前缀时模板链接不用跟着改:
<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 挂到某个路径前缀:
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 引用静态资源:
<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 提供了专门方法:
# 方式一(推荐):app.frontend() 内部使用 StaticFiles,
# 并额外处理了客户端路由(history 模式刷新 404 问题)
from fastapi import FastAPI
app = FastAPI()
# 前端构建产物放在 frontend/dist,API 与前端同域部署
app.frontend(directory="frontend/dist")经典的手写方案是"挂载 + catch-all 回退":
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 回退时,最需要注意什么?
🛠️ 动手实践
- 为一个"待办事项"应用同时实现
POST /api/todos(JSON API)和GET /todos(Jinja2 渲染的 HTML 列表页),列表页展示全部待办。 - 给第 1 题的页面加一个外部 CSS(通过
StaticFiles挂载并在模板中用url_for引用),把表格隔行变色。 - 把一个 Vue/React 构建产物(或手写的多页面静态站)用
app.frontend()或 catch-all 方式挂到 FastAPI 上,验证刷新子路径不 404。
下一章我们把单文件应用拆成规范的多模块工程:第 19 章 · 大型项目结构拆分。