FastAPI 结合 Jinja2 模板开发入门指南

FastAPI 结合 Jinja2 模板开发入门指南

全文约5500字,阅读约18分钟,跟着操作约40分钟完成第一个带页面的 Web 应用。

写在前面

WEB项目地址:演示地址

安卓APP下载地址:演示地址

上一篇文章我们用 FastAPI 写了一套 API 接口,返回的是 JSON 数据。但现实中的 Web 应用,除了给前端工程师调用的接口,很多时候还需要直接返回 HTML 页面------比如管理后台、官网、工具类网站。

FastAPI 本身是一个 API 框架,但它可以轻松集成 Jinja2 模板引擎,实现服务端渲染(SSR)。Jinja2 是 Flask 作者开发的模板系统,它让你在 HTML 里嵌入动态数据和控制逻辑,把 Python 后端和 HTML 前端分离开。本文从零开始,带你一步步搭建一个带页面的 FastAPI + Jinja2 项目。

前置要求:会基础的 Python,懂一点 HTML 标签就行。没看过上一篇的也不影响,本文从头讲。

一、开发环境搭建与依赖安装

1.1 创建虚拟环境

跟之前一样,先建一个虚拟环境:

bash 复制代码
# 创建
python -m venv jinja_env

# 激活(Mac/Linux)
source jinja_env/bin/activate

# 激活(Windows)
jinja_env\Scripts\activate

1.2 安装依赖

这里推荐一次性装好 fastapi[standard],它会把 FastAPI、Uvicorn 和 Jinja2 一起带上:

bash 复制代码
pip install "fastapi[standard]"

如果你已经装了 FastAPI,也可以单独补装 Jinja2:

bash 复制代码
pip install jinja2

注意 :Jinja2 不是 FastAPI 的默认依赖。如果后面遇到 jinja2 must be installed 的报错,就是忘了装这个包。

验证一下:

bash 复制代码
pip list | grep jinja2

能看到版本号就说明装好了。

二、Jinja2 模板引擎核心概念解析

在写代码之前,先搞清楚 Jinja2 是干什么的。

模板引擎的工作模式是"模板 + 数据 = 最终输出"。你把 HTML 页面写成模板,里面留一些"占位符",Jinja2 用 Python 传过来的数据替换这些占位符,生成完整的 HTML 字符串返回给浏览器。

Jinja2 的核心语法有三类:

语法 用途 示例
{``{ 变量名 }} 输出变量的值 {``{ username }}
{% 语句 %} 控制逻辑(判断、循环等) {% if user %}
{# 注释 #} 模板里的注释,不输出到页面 {# 这是一段注释 #}

后面我们会逐个用到。

三、项目目录结构与模板文件配置

3.1 创建目录结构

在项目根目录下建这样的文件夹:

复制代码
my_project/
├── app.py              # 主应用文件
├── templates/          # 存放所有 HTML 模板
│   └── index.html
└── static/             # 存放 CSS、JS、图片等静态文件
    └── style.css

templates 文件夹放 Jinja2 模板,static 文件夹放样式表、脚本、图片这些不需要服务端处理的文件。

3.2 编写第一个模板文件

templates/index.html 里写:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <h1>欢迎来到我的网站</h1>
    <p>当前时间:{{ current_time }}</p>
</body>
</html>

这里的 {``{ current_time }} 就是一个占位符,等会儿我们从 Python 传值进去。

3.3 配置 Jinja2Templates

app.py 里配置模板引擎:

python 复制代码
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()

# 配置模板目录
templates = Jinja2Templates(directory="templates")

Jinja2Templates(directory="templates") 告诉 FastAPI 去 templates 文件夹里找 HTML 文件。

四、基础路由创建与页面渲染流程

4.1 渲染第一个页面

python 复制代码
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from datetime import datetime

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    return templates.TemplateResponse(
        request=request,
        name="index.html",
        context={"current_time": now}
    )

逐行拆解:

  • @app.get("/", response_class=HTMLResponse) ------ 路由装饰器,response_class=HTMLResponse 告诉文档这个接口返回的是 HTML
  • request: Request ------ 必须传入 Request 对象,模板渲染需要它
  • templates.TemplateResponse() ------ 渲染模板并返回响应
  • name="index.html" ------ 要渲染的模板文件名
  • context={"current_time": now} ------ 传给模板的数据字典,模板里的 {``{ current_time }} 会被替换成这个值

版本提醒 :在 FastAPI 0.108.0 之前,name 是第一个参数,request 是作为 context 的一部分传递的。新版本里 request 要单独传。

4.2 启动服务

bash 复制代码
uvicorn app:app --reload

访问 http://127.0.0.1:8000,你应该能看到页面显示当前时间。

整个流程 是这样的:用户访问 / → FastAPI 匹配路由 → 调用 home() 函数 → 获取数据 now → 把数据和模板一起交给 Jinja2 → Jinja2 渲染出完整 HTML → 返回给浏览器。

五、模板变量传递与动态内容展示

5.1 传递多个变量

context 字典可以传任意多个变量:

python 复制代码
@app.get("/user/{user_id}", response_class=HTMLResponse)
async def user_profile(request: Request, user_id: int):
    # 模拟从数据库查数据
    user = {
        "id": user_id,
        "name": "张三" if user_id == 1 else "李四",
        "age": 25,
        "bio": "喜欢写代码"
    }
    return templates.TemplateResponse(
        request=request,
        name="profile.html",
        context={"user": user, "title": f"用户{user_id}的个人主页"}
    )

5.2 模板里使用变量

templates/profile.html

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ user.name }} 的个人资料</h1>
    <p>用户ID:{{ user.id }}</p>
    <p>年龄:{{ user.age }}</p>
    <p>简介:{{ user.bio }}</p>
</body>
</html>

访问 http://127.0.0.1:8000/user/1,页面会显示张三的信息。{``{ user.name }} 这种点号语法可以访问字典的 key 或对象的属性。

5.3 变量不存在时怎么办

如果模板里引用了 context 中没有的变量,Jinja2 不会报错,而是显示空字符串。你也可以用 default 过滤器给默认值:

html 复制代码
<p>{{ user.phone | default('未填写') }}</p>

六、控制语句使用:条件判断与循环遍历

6.1 条件判断 {% if %}

模板里可以做逻辑判断。比如用户列表页面,有数据时展示表格,没数据时显示提示:

html 复制代码
{% if users %}
    <ul>
    {% for user in users %}
        <li>{{ user.name }} - {{ user.age }}岁</li>
    {% endfor %}
    </ul>
{% else %}
    <p>暂无用户数据</p>
{% endif %}

6.2 循环遍历 {% for %}

后端传一个列表过来:

python 复制代码
@app.get("/users", response_class=HTMLResponse)
async def list_users(request: Request):
    users = [
        {"id": 1, "name": "张三", "age": 25},
        {"id": 2, "name": "李四", "age": 30},
        {"id": 3, "name": "王五", "age": 28},
    ]
    return templates.TemplateResponse(
        request=request,
        name="users.html",
        context={"users": users}
    )

模板里循环输出:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>用户列表</title>
</head>
<body>
    <h1>用户列表(共 {{ users | length }} 人)</h1>
    <table border="1">
        <tr>
            <th>ID</th>
            <th>姓名</th>
            <th>年龄</th>
        </tr>
        {% for user in users %}
        <tr>
            <td>{{ user.id }}</td>
            <td>{{ user.name }}</td>
            <td>{{ user.age }}</td>
        </tr>
        {% endfor %}
    </table>
</body>
</html>

{``{ users | length }} 用到了过滤器(filter),length 是 Jinja2 内置的,用来获取列表长度。类似的过滤器还有 upperlowerdefault 等。

6.3 循环中的特殊变量

{% for %} 循环里,Jinja2 提供了一些特殊变量:

变量 说明
loop.index 当前迭代次数(从1开始)
loop.index0 当前迭代次数(从0开始)
loop.first 是否是第一个元素
loop.last 是否是最后一个元素
html 复制代码
{% for user in users %}
    <tr>
        <td>{{ loop.index }}</td>
        <td>{{ user.name }}</td>
    </tr>
{% endfor %}

七、模板继承机制与代码复用技巧

7.1 为什么需要模板继承

一个网站通常有几十个页面,如果每个页面都写一遍完整的 HTML 结构(head、导航栏、页脚),维护起来非常痛苦。模板继承就是为了解决这个问题------把公共部分抽出来,子页面只写自己独有的内容。

7.2 创建父模板 base.html

templates/base.html 里写:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}我的默认网站{% endblock %}</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <!-- 公共导航栏 -->
    <nav>
        <a href="/">首页</a> | 
        <a href="/users">用户列表</a> | 
        <a href="/about">关于</a>
    </nav>
    <hr>

    <!-- 核心内容区------子模板来填充 -->
    <div class="content">
        {% block content %}
        {% endblock %}
    </div>

    <!-- 公共页脚 -->
    <hr>
    <footer>© 2026 我的网站</footer>
</body>
</html>

关键语法:{% block 块名 %}{% endblock %} 在父模板里预留一个"坑",等着子模板来填。

7.3 子模板继承父模板

templates/users.html

html 复制代码
{% extends "base.html" %}

{% block title %}用户列表 - 我的网站{% endblock %}

{% block content %}
    <h1>用户列表</h1>
    <table border="1">
        <tr>
            <th>ID</th>
            <th>姓名</th>
            <th>年龄</th>
        </tr>
        {% for user in users %}
        <tr>
            <td>{{ user.id }}</td>
            <td>{{ user.name }}</td>
            <td>{{ user.age }}</td>
        </tr>
        {% endfor %}
    </table>
{% endblock %}
  • {% extends "base.html" %} 必须在第一行声明继承哪个父模板
  • 然后用 {% block title %}{% block content %} 重写父模板里对应的块
  • 没重写的块(比如父模板里没有别的块了)就保持父模板的默认内容

注意 :Jinja2 默认不支持 {% extends "../base.html" %} 这种带 .. 的相对路径写法,请一律从模板根目录开始写路径。

7.4 在父模板里保留子模板的内容

有时候你想在父模板里包一层东西,同时保留子模板的内容,可以用 {``{ super() }}

html 复制代码
{% block content %}
    <div class="container">
        {{ super() }}
    </div>
{% endblock %}

八、静态资源管理与样式表引入

8.1 挂载静态文件目录

app.py 里配置静态文件服务:

python 复制代码
from fastapi.staticfiles import StaticFiles

app = FastAPI()

# 挂载静态文件目录
app.mount("/static", StaticFiles(directory="static"), name="static")

# 模板配置...

app.mount("/static", StaticFiles(directory="static"), name="static") 的意思是:访问 /static/xxx 时,去 static 文件夹找对应的文件。

8.2 在模板中引用静态文件

base.html 里引用 CSS:

html 复制代码
<link rel="stylesheet" href="{{ url_for('static', path='/style.css') }}">

url_for('static', path='/style.css') 会生成 /static/style.css 这个 URL。

注意url_for 的第一个参数 'static' 必须跟 app.mount() 里的 name="static" 保持一致。如果名字写错了,模板里会找不到路径。

8.3 写一点样式

static/style.css

css 复制代码
body {
    font-family: Arial, sans-serif;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
    background-color: #f5f5f5;
}

nav {
    background-color: #333;
    padding: 10px;
}

nav a {
    color: white;
    text-decoration: none;
    margin-right: 15px;
}

.content {
    background: white;
    padding: 20px;
    border-radius: 5px;
    min-height: 300px;
}

footer {
    text-align: center;
    color: #666;
    padding: 10px;
}

刷新页面,样式就生效了。

九、表单数据处理与用户交互实现

9.1 创建登录页面

templates/login.html

html 复制代码
{% extends "base.html" %}

{% block title %}登录{% endblock %}

{% block content %}
    <h1>用户登录</h1>
    {% if message %}
        <p style="color: red;">{{ message }}</p>
    {% endif %}
    <form method="post">
        <div>
            <label for="username">用户名:</label>
            <input type="text" id="username" name="username" required>
        </div>
        <div>
            <label for="password">密码:</label>
            <input type="password" id="password" name="password" required>
        </div>
        <button type="submit">登录</button>
    </form>
{% endblock %}

表单里的 name="username"name="password" 很重要------后端接收表单数据时就是根据这个 name 来取值的。

9.2 后端处理 GET 和 POST

python 复制代码
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
    return templates.TemplateResponse(
        request=request,
        name="login.html",
        context={"message": None}
    )

@app.post("/login", response_class=HTMLResponse)
async def login_submit(
    request: Request,
    username: str = Form(...),
    password: str = Form(...)
):
    # 简单验证------实际项目里应该查数据库
    if username == "admin" and password == "123456":
        # 登录成功,重定向到首页
        return RedirectResponse(url="/", status_code=302)
    else:
        # 登录失败,回到登录页并显示错误信息
        return templates.TemplateResponse(
            request=request,
            name="login.html",
            context={"message": "用户名或密码错误"}
        )

关键点:

  • @app.post("/login") 处理表单提交
  • username: str = Form(...) 从表单里提取 username 字段的值
  • Form(...) 里的 ... 表示该字段必填
  • 登录成功后用 RedirectResponse 跳转,失败则重新渲染登录页并带上错误信息

9.3 表单提交的注意事项

  1. 表单的 method 必须是 post (跟后端的 @app.post 对应)
  2. input 的 name 属性 必须跟后端 Form(...) 里的变量名一致
  3. FastAPI 接收表单数据和接收 JSON 是两套机制,用 Form 就不能同时用 Body

十、常见渲染报错排查与调试方法

10.1 jinja2 must be installed to use Jinja2Templates

报错AssertionError: jinja2 must be installed to use Jinja2Templates

原因:没装 Jinja2。

解决

bash 复制代码
pip install jinja2

10.2 TemplateNotFound: index.html

报错jinja2.exceptions.TemplateNotFound: index.html

原因:模板文件不存在,或者路径不对。

解决

  • 检查 templates/ 文件夹里有没有 index.html
  • 检查 Jinja2Templates(directory="templates") 的路径是否正确
  • 确认启动命令是在项目根目录执行的

10.3 TypeError: unhashable type: 'dict'

报错 :渲染模板时报 TypeError: unhashable type: 'dict'

原因 :Starlette 新版本的 TemplateResponse 签名变了,跟旧版调用方式不兼容。

解决 :确保 TemplateResponse 的调用方式跟本文示例一致------request 作为单独的参数传入,不要塞进 context 里。

10.4 url_for('static', ...) 找不到路径

报错 :模板里 url_for('static', path='/style.css') 生成不了正确的 URL

原因app.mount() 里的 nameurl_for 的第一个参数不一致。

解决:保持名字统一:

python 复制代码
app.mount("/static", StaticFiles(directory="static"), name="static")  # name 是 "static"
html 复制代码
<link href="{{ url_for('static', path='/style.css') }}" rel="stylesheet">  <!-- 第一个参数也是 "static" -->

10.5 模板修改后页面没变化

原因 :服务启动时没加 --reload

解决

bash 复制代码
uvicorn app:app --reload

10.6 调试小技巧

在模板里打印变量 :Jinja2 没有 print,但可以用 {% debug %} 输出当前上下文的所有变量(需要开启调试模式)。

查看渲染后的 HTML:在浏览器里右键"查看网页源代码",看到的就是 Jinja2 渲染完成后的最终 HTML。如果哪里不对,对比源代码和模板就能定位问题。

后端打印日志

python 复制代码
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@app.post("/login")
async def login_submit(username: str = Form(...), password: str = Form(...)):
    logger.info(f"登录尝试: {username}")
    # ...

小结

到这里,你已经掌握了 FastAPI + Jinja2 的完整开发流程:

  1. 环境搭建 ------虚拟环境 + 安装 fastapi[standard]
  2. 模板配置 ------Jinja2Templates(directory="templates")
  3. 页面渲染 ------TemplateResponse(request, name, context)
  4. 变量传递 ------{``{ 变量名 }} 输出数据
  5. 控制语句 ------{% if %}{% for %} 做逻辑
  6. 模板继承 ------{% extends %} + {% block %} 复用布局
  7. 静态资源 ------app.mount() + url_for()
  8. 表单处理 ------Form(...) 接收表单数据

下一步可以做什么

  • 把模板继承用起来,把导航栏、页脚、全局样式抽到 base.html
  • 接入数据库(SQLite 或 PostgreSQL),让用户数据和文章数据持久化
  • @app.middleware("http") 加一个简单的登录状态校验
  • 试试 HTMX ------ 在 Jinja2 模板里用 AJAX 做局部刷新,不用写 JavaScript

写代码遇到问题,先检查三点:虚拟环境激活了没、依赖装全了没、服务是不是带着 --reload 启动的。大部分坑都出在这三个地方。

相关推荐
初学AI的小高1 天前
FastAPI 工程实践:LLM 应用的 API 设计之道
后端·fastapi
java1234_小锋1 天前
FastAPI python web开发- 中间件
fastapi
不能只会打代码1 天前
Day 002 — Python 工程化 & FastAPI & 数据库速通
数据库·redis·python·docker·fastapi·测试
allnlei1 天前
React + FastAPI 前后端分离架构下的 Nginx 配置实践
react.js·架构·fastapi
诸神缄默不语2 天前
FastAPI后端配置CORS中间件支持浏览器跨域访问
后端·fastapi
开源技术2 天前
Fastapi教程: 身份认证与授权03——带有 password 和 Bearer 的简单 OAuth2
fastapi
java1234_小锋3 天前
FastAPI python web开发- 表单数据与模型 & 请求表单与文件 & 响应类型 - 返回文件类型
fastapi
hboot4 天前
AI工程师第五课 - 大语言模型基础
python·llm·fastapi
喜欢的名字被抢了4 天前
Python实战:SQLAlchemy ORM与FastAPI项目集成
开发语言·python·sql·教程·fastapi