FastAPI 模板渲染实战:使用 Jinja2Templates 构建服务端渲染页面
导读
本文围绕 FastAPI 官方文档中 advanced/templates 一页(docs/hi/docs/advanced/templates.md),系统讲解如何在 FastAPI 中接入模板引擎(以 Jinja2 为例)进行服务端页面渲染。读完本文,你将掌握模板依赖的安装方式、Jinja2Templates 的正确使用方法与参数签名、如何在 Jinja2 模板内渲染上下文变量、调用 url_for() 生成路由链接并挂载静态文件,同时结合当前仓库源码理解其与 Starlette 的底层关系及测试验证方式。
模板引擎:FastAPI 并不限定某一家
FastAPI 官方文档明确指出:你可以在 FastAPI 中使用任何你喜欢的模板引擎(template engine)。模板引擎负责把「HTML 骨架 + 动态数据」合成完整的 HTML 页面,是服务端渲染(Server-Side Rendering)的核心一环。
在实际项目中,最普遍的选择是 Jinja2——它同样是 Flask 等其他知名工具使用的模板引擎,语法简单、生态成熟。更关键的是,FastAPI 官方为 Jinja2 提供了「开箱即用」的配置工具类,你可以直接在 FastAPI application 里使用它,而这些工具实际上由 Starlette(FastAPI 底层的 ASGI 框架)提供。
一句话归纳:FastAPI 提供便捷入口,Starlette 负责具体实现,两者配合让你专注写页面逻辑而不必关心集成细节。
安装依赖:为项目加入 jinja2
使用 Jinja2Templates 前,需要先把 jinja2 加入项目依赖。仓库使用的示例命令是 uv:
$ uv add jinja2
---> 100%
命令执行成功后,jinja2 会写入项目的依赖清单(对应当前仓库的 uv.lock / pyproject.toml 管理机制)。如果你习惯使用其他包管理器,也可以用 pip install jinja2 或 poetry add jinja2 达到同样效果,核心是保证运行环境中能够 import jinja2。
使用 Jinja2Templates:四个固定步骤
在 FastAPI 中接入 Jinja2 的标准流程,可以拆解为以下四个步骤:
- 导入
Jinja2Templates; - 创建一个可复用的
templates对象(指向存放模板文件的目录); - 在将要返回模板的 path operation 中声明一个
Request参数; - 使用前面创建的
templates对象渲染并返回TemplateResponse,传入三个要素:模板文件名name、request对象、包含 key-value 对的「context」字典(context 中的键值会被 Jinja2 在模板内部直接引用)。
仓库中对应的完整可运行示例位于 docs_src/templates/tutorial001_py310.py,核心代码如下:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.get("/items/{id}", response_class=HTMLResponse)
async def read_item(request: Request, id: str):
return templates.TemplateResponse(
request=request, name="item.html", context={"id": id}
)
逐行拆解这段代码中的关键细节:
templates = Jinja2Templates(directory="templates"):创建模板对象并指向templates/目录。这里directory是相对当前工作目录的路径,也可以传绝对路径;你完全可以只创建一次然后在多个路由中复用它。def read_item(request: Request, id: str):必须声明request: Request参数,因为TemplateResponse需要它(它的作用之一是后续在模板里通过url_for正确生成 URL)。return templates.TemplateResponse(...):注意示例使用的调用方式是关键字参数——request=、name=、context=。其中context字典里的id会被模板读取渲染。response_class=HTMLResponse:建议声明。这能让/docs交互式文档界面(Swagger UI)知道该接口返回的是 HTML 内容,从而在 OpenAPI schema 中正确标注 media type。
💡 Tip(来自原文档):声明
response_class=HTMLResponse后,docs UI 才能正确识别响应是 HTML。
版本兼容提示:TemplateResponse 的参数签名演进
官方文档特别标注了一条重要的版本兼容信息(详见 docs/hi/docs/advanced/templates.md 的 note 区块):
- 在 FastAPI 0.108.0 / Starlette 0.29.0 之前,
TemplateResponse的第一个参数是name,即调用方式为templates.TemplateResponse("item.html", {"request": request, "id": id})这类位置参数写法; - 同样在更早的版本中,
request对象需要作为普通键值对手工放进 Jinja2 的 context 里(例如{"request": request, ...}),而不是像现在这样作为独立的request=关键字传入。
如果你在旧版代码或网上旧教程中看到类似 TemplateResponse("name.html", {"request": request}) 的写法,请留意它与当前新签名(TemplateResponse(request=request, name="name.html", context={...}))的差异。当前仓库的示例统一采用新签名,升级项目时应同步迁移。
编写模板:templates/item.html
创建好 templates 对象并声明路由后,接下来就是在模板目录中编写真正的 HTML 模板文件。仓库里对应的文件是 docs_src/templates/templates/item.html,内容如下:
<html>
<head>
<title>Item Details</title>
<link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">
</head>
<body>
<h1><a href="{{ url_for('read_item', id=id) }}">Item ID: {{ id }}</a></h1>
</body>
</html>
模板上下文(Template Context Values)
Jinja2 用 {{ 变量名 }} 双花括号语法做变量插值。模板中这一行:
Item ID: {{ id }}
其中 {{ id }} 会从你传入的 context 字典中取出 id 对应的值:
{"id": id}
也就是说,context 字典的 key 就是模板内可直接引用的变量名。例如当请求路径为 /items/42(即 id 为 42)时,最终渲染出的 HTML 就是:
Item ID: 42
模板内调用 url_for():生成路由链接
在 Jinja2 模板里你同样可以使用 url_for(),它接受的参数与你 path operation function 使用的参数一致,用于反向生成路由对应的 URL。
例如模板中:
<a href="{{ url_for('read_item', id=id) }}">
这会生成一条指向「处理 read_item(id=id) 的 path operation」的链接。当 id 为 42 时,渲染结果为:
<a href="/items/42">
这个机制的巧妙之处在于:URL 只定义一次(在路由装饰器中),模板侧通过函数名 + 参数反向生成,避免在 HTML 中硬编码路径——将来即使改了路由路径,只要函数名和参数不变,模板无需改动。这一点也印证了 Request 参数为何必须传入:url_for 需要借助当前请求上下文来解析完整、正确的 URL。
模板与静态文件:Jinja2 × StaticFiles
服务端渲染页面几乎离不开 CSS / JS / 图片等静态资源。FastAPI + Jinja2 的组合方式非常直接:
- 先用
app.mount("/static", StaticFiles(directory="static"), name="static")挂载静态目录,并给挂载点命名name="static"; - 在模板内通过
url_for('static', path='/styles.css')引用具体文件。
前面 item.html 中的 <head> 部分正是这样做的:
<link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">
它生成的地址会指向 static/styles.css。仓库对应的样式文件为 docs_src/templates/static/styles.css:
h1 {
color: green;
}
由于你挂载了 StaticFiles,这个 CSS 文件会被 FastAPI application 在 URL /static/styles.css 上自动对外提供(对应的实际目录文件是 static/styles.css),无需再手写任何静态文件路由。
注意此处 url_for 的第二个参数是 path='/styles.css'——它表示相对于静态挂载点的子路径,最终拼接出 /static/styles.css。
整体运行与测试验证
把上述三块拼起来(docs_src/templates/tutorial001_py310.py + templates/item.html + static/styles.css),并保证目录结构如下:
./
├── main.py # 应用入口(含上述代码)
├── templates/
│ └── item.html
└── static/
└── styles.css
启动方式与普通 FastAPI 应用一致:
$ uvicorn main:app --reload
然后访问:
http://localhost:8000/items/42→ 返回渲染后的 HTML 页面(绿色标题、可点击链接Item ID: 42);http://localhost:8000/static/styles.css→ 直接返回 CSS 文件内容;http://localhost:8000/docs→ 看到接口响应类型被标注为 HTML。
仓库的自动化测试也完整覆盖了这一示例,测试文件位于 tests/test_tutorial/test_templates/test_tutorial001.py。它使用 TestClient 对该示例做了端到端断言,值得对照阅读:
client = TestClient(app)
response = client.get("/items/foo")
assert response.status_code == 200, response.text
assert (
b'<h1><a href="http://testserver/items/foo">Item ID: foo</a></h1>'
in response.content
)
response = client.get("/static/styles.css")
assert response.status_code == 200, response.text
assert b"color: green;" in response.content
测试逻辑透露出三个要点:
- 动态路由
/items/{id}能正确渲染模板并把上下文值(这里是foo)写入 HTML; url_for('static', ...)在TestClient环境下会解析出带 host 的完整 URL(http://testserver/...),进一步说明Request与请求上下文参与 URL 生成;- 挂载的
/static/styles.css能独立、正常地返回 CSS 文件。
从测试结构还可看出一个工程细节:该测试需要先把
docs_src/templates/下的templates/与static/目录复制到测试运行目录,原因是示例中使用了相对路径directory="templates"与directory="static",即路径解析依赖「当前工作目录」。这提醒我们在真实项目中尽量使用确定的工作目录或绝对路径来定位模板与静态目录,避免因启动目录不同导致FileNotFoundError。
源码层面:FastAPI 与 Starlette 的关系
如果你想追根究底,可以打开仓库中的 fastapi/templating.py,它的全部内容只有一行:
from starlette.templating import Jinja2Templates as Jinja2Templates # noqa
这行代码直观说明了原文档「技术细节」区块所强调的事实:
Jinja2Templates的真正实现在 Starlette 的starlette.templating模块中;- FastAPI 只是出于开发者便利,把同一个类原样转发导出为
fastapi.templating.Jinja2Templates,所以下面两种导入方式完全等价:
from fastapi.templating import Jinja2Templates # 推荐,FastAPI 统一入口
from starlette.templating import Jinja2Templates # 同样可行,直接走底层
实际上,不只是模板系统,Request 和 StaticFiles 等大多数响应与请求基础组件同样直接源自 Starlette——FastAPI 在此之上提供了自动校验、序列化、OpenAPI 文档等「增量价值」。理解了这层转发关系,你在排查模板渲染问题时就清楚应该去查阅 Starlette 对 Jinja2Templates(以及它内部的 env、context_processor 等机制)的实现约定。
延伸:更多细节去哪里找
本文覆盖的是模板渲染的完整主干流程。关于更深入的内容——例如如何在测试中校验模板、如何自定义模板环境、上下文处理器等——原文档建议进一步参阅 Starlette 官方的模板文档。
如果你希望在本仓库内继续动手验证,推荐按如下顺序阅读:
- 完整示例源码:docs_src/templates/tutorial001_py310.py
- 模板文件:docs_src/templates/templates/item.html
- 静态样式:docs_src/templates/static/styles.css
- FastAPI 的转发实现:fastapi/templating.py
- 端到端测试:tests/test_tutorial/test_templates/test_tutorial001.py
小结:FastAPI 服务端渲染的最小心法
在 FastAPI 中使用 Jinja2 渲染服务端页面,只需记住四条主线:装 jinja2 → 建 Jinja2Templates(directory=...) → 路由里声明 Request 并用关键字参数 request=... / name=... / context=... 返回 TemplateResponse → 在模板里用 {{ 变量 }} 插值、用 url_for() 反查路由与静态资源。若遇到旧写法(name 作为第一参数、request 塞进 context),请对照版本迁移说明进行更新。这套模式把 FastAPI 的接口能力与 Jinja2 的模板能力无缝衔接,足以支撑从简单页面到较完整服务端渲染应用的实际开发。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00