
文章目录
1. 课前导读
本节课学习目标
在前8课中,我们专注于构建RESTful API接口,返回JSON数据。但在实际Web开发中,有时需要提供HTML页面(如管理后台、邮件模板预览、文件上传后的展示页面)和静态资源(CSS、JavaScript、图片)。FastAPI虽然主打API开发,但也内置了静态文件托管和模板渲染的支持,让开发者可以轻松搭建小型全栈应用或混合架构。
学完本节课,你将:
- 掌握静态文件托管:使用
StaticFiles中间件挂载目录,提供CSS、JS、图片等资源 - 配置Jinja2模板引擎:集成FastAPI与Jinja2,渲染动态HTML页面
- 理解模板上下文传递:将后端数据(如用户信息、列表)注入到模板中
- 实现文件上传后的预览页面:结合第8课,生成上传图片的HTML展示
- 开发简单管理面板:使用模板+静态文件搭建后台界面
- 了解前后端混合模式:什么时候用模板,什么时候用纯API
前置知识
- 已完成第8课,掌握文件上传
- 基础HTML/CSS知识(了解基本标签即可)
- 了解Jinja2模板语法(变量输出、循环、条件,本节会讲解)
学完能掌握什么
学完本节课后,你将具备以下能力:
- 配置静态文件服务:让FastAPI应用能够对外提供图片、样式表、JavaScript文件
- 使用Jinja2渲染HTML:生成动态页面,并利用模板继承提高复用性
- 构建简单的全栈功能:例如文件上传后的展示页面、数据可视化报表页面
- 理解API与模板混合模式:在同一个项目中,部分路由返回JSON,部分返回HTML
- 为后续课程打基础:某些后台管理界面或第三方登录回调页面可能需要模板
适用人群
- 需要用FastAPI开发包含简单前端界面的项目(如内部工具、原型演示)
- 需要生成邮件HTML内容或报告页面的开发者
- 希望了解FastAPI非API能力的全栈工程师
- 准备集成前端框架(React/Vue)但需要静态文件服务的学员
2. 核心理论讲解
2.1 静态文件托管的基本原理
在传统的Web服务器(如Nginx)中,静态文件(图片、CSS、JS)通常直接由服务器处理,不经过应用层。在FastAPI中,我们可以使用StaticFiles来挂载一个目录,使得该目录下的文件可以通过HTTP直接访问。
工作原理:
StaticFiles是一个ASGI应用,FastAPI将其挂载到某个路径前缀下(如/static)。- 当请求
/static/css/style.css时,FastAPI会将请求转发给StaticFiles实例。 StaticFiles将请求路径映射到文件系统路径,读取文件并返回,同时自动推断Content-Type。
与Django/Flask对比:
- Flask:需要
send_from_directory或扩展flask-static - Django:
django.contrib.staticfiles,需要collectstatic命令 - FastAPI:简单直接,适合微服务或小型项目,生产环境仍推荐Nginx/CDN
2.2 模板引擎的角色
模板引擎允许开发者编写包含占位符和逻辑的HTML文件,在后端填充数据后生成最终HTML响应。FastAPI官方推荐Jinja2,它与Flask使用相同的模板引擎,语法强大且易用。
工作流程:
- 后端路由函数准备数据(字典)。
- 调用
TemplateResponse,传入模板文件名和上下文数据。 - Jinja2加载模板文件,解析变量、循环、条件等,渲染为HTML字符串。
- FastAPI返回
text/html响应。
2.3 FastAPI集成Jinja2的方式
FastAPI本身没有内置模板引擎,但可以通过jinja2库和自定义响应类实现。官方提供了便捷的集成方式:使用Jinja2Templates。
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
@app.get("/")
def home(request: Request):
return templates.TemplateResponse("index.html", {"request": request, "name": "FastAPI"})
注意:必须传递request参数,因为Jinja2Templates需要它来构建URL等。
2.4 模板与API的混合架构决策
在开发一个Web项目时,通常会面临选择:使用前后端分离(React/Vue + API)还是服务端渲染(模板)。FastAPI可以同时支持两者。
| 场景 | 推荐方案 |
|---|---|
| 纯后端服务(供移动端/第三方调用) | 仅JSON API |
| 内部管理工具(简单界面,不追求极致交互) | 模板 + 少量JavaScript |
| 面向用户的复杂交互应用 | 前后端分离(API + 前端框架) |
| 需要SEO优化的内容型网站 | 模板(SSR)或现代SSR框架 |
本节课将展示如何在FastAPI中混合使用API和模板,让你灵活应对各种需求。
3. 环境搭建 & 实操准备
3.1 复用项目环境
继续使用第8课的项目。确保虚拟环境已激活。
cd ~/fastapi_course/lesson03_first_project
source venv/bin/activate
3.2 安装Jinja2
pip install jinja2
3.3 创建模板和静态文件目录
mkdir templates
mkdir static
mkdir static/css
mkdir static/js
mkdir static/images
3.4 编写基础静态文件示例
创建static/css/style.css:
body {
font-family: Arial, sans-serif;
margin: 40px;
background-color: #f5f5f5;
}
h1 {
color: #333;
}
.container {
max-width: 800px;
margin: auto;
background: white;
padding: 20px;
border-radius: 8px;
}
创建static/js/script.js:
console.log("静态文件加载成功");
3.5 创建模板文件
创建templates/base.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 %}FastAPI 教程{% endblock %}</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<header>
<h1>{% block header %}FastAPI 示例站点{% endblock %}</h1>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2025 FastAPI 从入门到精通</p>
</footer>
</div>
<script src="/static/js/script.js"></script>
</body>
</html>
创建templates/index.html(首页):
{% extends "base.html" %}
{% block title %}首页 - FastAPI教程{% endblock %}
{% block header %}欢迎来到FastAPI世界{% endblock %}
{% block content %}
<p>这是通过Jinja2模板渲染的HTML页面。</p>
<p>当前用户: {{ user_name }}</p>
<ul>
<li><a href="/static/css/style.css">查看CSS文件</a></li>
<li><a href="/upload-demo">文件上传演示</a></li>
<li><a href="/api/v1/upload/single-file">API示例</a></li>
</ul>
{% endblock %}
3.6 在main.py中配置静态文件和模板
编辑app/main.py,添加以下内容:
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi import Request
# 在创建 app 对象后,添加静态文件挂载
app.mount("/static", StaticFiles(directory="static"), name="static")
# 配置模板引擎
templates = Jinja2Templates(directory="templates")
然后新增一个路由返回HTML页面(可以在main.py或单独的路由文件中)。为了清晰,我们在app/api/v1/下新建一个web_pages.py,但为了简单,直接在main.py中添加几个HTML路由。
由于我们项目结构中已经有app/main.py作为主入口,我们可以在其中增加几个不挂载前缀的页面路由(根路径等)。但为了保持模块化,我们单独创建一个路由文件并包含到主应用。
创建app/api/v1/web.py:
touch app/api/v1/web.py
内容如下:
# app/api/v1/web.py
from fastapi import APIRouter, Request, Form, File, UploadFile
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
from pathlib import Path
import shutil
from datetime import datetime
router = APIRouter(tags=["网页界面"])
# 注意:模板对象需要从main传递或重新创建。为了简单,我们在这里重新创建templates对象(路径相对于项目根目录)
# 在实际项目中,更好的做法是将templates对象放在一个共享模块中。
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
@router.get("/", response_class=HTMLResponse)
async def home(request: Request):
return templates.TemplateResponse("index.html", {"request": request, "user_name": "Guest"})
@router.get("/upload-demo", response_class=HTMLResponse)
async def upload_demo_page(request: Request):
return templates.TemplateResponse("upload.html", {"request": request})
然后在app/main.py中引入该路由,但注意不要加API前缀,让它挂载在根路径:
from app.api.v1 import web
app.include_router(web.router) # 没有prefix,直接注册在根
同时,我们需要把之前在main.py中的根路径@app.get("/")移除或注释,避免冲突。
创建templates/upload.html:
{% extends "base.html" %}
{% block title %}文件上传演示{% endblock %}
{% block content %}
<h2>上传文件</h2>
<form action="/api/v1/upload/single-file" method="post" enctype="multipart/form-data">
<input type="file" name="file" required>
<button type="submit">上传</button>
</form>
<hr>
<h3>上传结果</h3>
<div id="result"></div>
<script>
// 可选:使用fetch提交表单并显示结果,避免跳转
const form = document.querySelector('form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const response = await fetch('/api/v1/upload/single-file', {
method: 'POST',
body: formData
});
const data = await response.json();
document.getElementById('result').innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
});
</script>
{% endblock %}
3.7 运行并测试
启动服务:
uvicorn app.main:app --reload
访问 http://localhost:8000/ 查看首页,访问 http://localhost:8000/upload-demo 查看文件上传页面。
4. 手把手代码实战
4.1 基础静态文件托管
代码示例(已在main.py中完成):
app.mount("/static", StaticFiles(directory="static"), name="static")
- 第一个参数
"/static"是URL路径前缀。 directory="static"是本地文件夹路径(相对项目根目录)。name用于反向URL解析。
访问静态文件:http://localhost:8000/static/css/style.css
4.2 模板渲染基础
代码示例(web.py中的home路由):
@router.get("/", response_class=HTMLResponse)
async def home(request: Request):
return templates.TemplateResponse("index.html", {"request": request, "user_name": "Alice"})
要点:
- 必须传递
request对象,模板中可以通过{{ request.url }}等获取请求信息。 - 可以传递任意额外的变量,在模板中使用
{{ variable_name }}输出。 response_class=HTMLResponse可选,但明确指定有利于文档。
4.3 模板语法快速入门
Jinja2常用的语法:
| 语法 | 示例 | 说明 |
|---|---|---|
| 变量输出 | {{ name }} | 输出变量值 |
| 变量属性 | {{ user.name }} | 访问字典或对象属性 |
| 过滤器 | {{ name | upper }} | 转换为大写 |
| 循环 | {% for item in items %}{{ item }}{% endfor %} | 遍历列表 |
| 条件判断 | {% if user.is_admin %}Admin{% else %}User{% endif %} | 分支逻辑 |
| 模板继承 | {% extends "base.html" %} | 继承父模板 |
| 代码块 | {% block content %}{% endblock %} | 定义可覆盖的区域 |
| 包含 | {% include "header.html" %} | 引入子模板 |
| 安全输出 | {{ html_content | safe }} | 不转义HTML(慎用) |
4.4 实战:动态数据展示 - 商品列表页面
假设我们有一个商品列表API,现在想要一个HTML页面展示商品。
首先,在web.py中添加一个新路由:
from app.api.v1.http_methods_demo import fake_products_db # 复用之前的数据
@router.get("/products", response_class=HTMLResponse)
async def products_page(request: Request):
products = list(fake_products_db.values())
return templates.TemplateResponse("products.html", {"request": request, "products": products})
创建templates/products.html:
{% extends "base.html" %}
{% block title %}商品列表{% endblock %}
{% block content %}
<h2>商品列表</h2>
<table border="1" cellpadding="8">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>价格</th>
<th>库存</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr>
<td>{{ product.id }}</td>
<td>{{ product.name }}</td>
<td>{{ product.price }}</td>
<td>{{ product.stock }}</td>
<td>{{ product.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><a href="/upload-demo">上传新商品?暂时没有商品创建表单,可通过API添加</a></p>
{% endblock %}
访问 http://localhost:8000/products 查看商品列表(需要事先通过API创建一些商品)。
4.5 实战:文件上传后的预览页面
结合第8课,我们可以在文件上传成功后返回一个HTML页面,显示上传的文件信息并提供预览(如果是图片)。修改file_upload.py中的上传接口,增加一个可选参数return_html,但更简单的是单独做一个用于网页的上传接口。
在web.py中添加一个专门的上传处理路由,返回HTML:
@router.post("/upload-file", response_class=HTMLResponse)
async def upload_file_for_web(
request: Request,
file: UploadFile = File(...)
):
# 保存文件
safe_filename = f"{datetime.now().timestamp()}_{file.filename}"
file_path = Path("uploads") / safe_filename
file_path.parent.mkdir(exist_ok=True)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# 判断是否为图片,以便预览
is_image = file.content_type and file.content_type.startswith("image/")
preview_url = f"/uploads/{safe_filename}" if is_image else None
return templates.TemplateResponse("upload_result.html", {
"request": request,
"filename": file.filename,
"saved_name": safe_filename,
"size": file_path.stat().st_size,
"is_image": is_image,
"preview_url": preview_url
})
同时需要挂载uploads目录为静态目录(注意安全,生产环境应避免直接暴露上传目录,这里仅为演示):
# 在 main.py 中添加(注意:仅用于开发,生产应使用Nginx)
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
创建templates/upload_result.html:
{% extends "base.html" %}
{% block title %}上传成功{% endblock %}
{% block content %}
<h2>文件上传成功</h2>
<ul>
<li>原文件名: {{ filename }}</li>
<li>保存为: {{ saved_name }}</li>
<li>大小: {{ size }} 字节</li>
</ul>
{% if is_image %}
<h3>图片预览</h3>
<img src="{{ preview_url }}" alt="预览" style="max-width: 100%;">
{% endif %}
<p><a href="/upload-demo">继续上传</a> | <a href="/products">商品列表</a></p>
{% endblock %}
4.6 模板进阶:表单与CSRF保护
在生产环境中,HTML表单通常需要CSRF保护。FastAPI没有内置,可以使用fastapi-csrf等第三方库,或者通过依赖注入方式手动验证。由于这不是本课重点,可以暂时忽略,但在真实项目中需要关注。
4.7 结合API和模板:实现一个简单的管理后台
我们可以创建一个简单的管理后台仪表盘,通过API获取数据并以模板展示。例如,使用httpx在模板后端调用自己的API(避免暴露前端直接调用API的细节)。
在web.py中添加:
import httpx
@router.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request):
# 调用内部API获取数据
async with httpx.AsyncClient() as client:
resp = await client.get("http://localhost:8000/api/v1/http/products")
products = resp.json()
stats = {
"product_count": len(products),
"total_stock": sum(p["stock"] for p in products),
}
return templates.TemplateResponse("dashboard.html", {"request": request, "stats": stats, "products": products[:5]})
创建templates/dashboard.html,展示统计数据。
4.8 使用模板中的URL构建
在模板中可以通过url_for构建动态URL,但Jinja2Templates默认没有注入该函数。可以自定义上下文处理器,或者直接硬编码。简单方法:在渲染时传入request,用{{ request.url_for('static', path='css/style.css') }}。但需注意路由命名。
4.9 错误页面定制
可以自定义404、500等错误页面。例如:
from fastapi.exceptions import HTTPException
from starlette.exceptions import HTTPException as StarletteHTTPException
@app.exception_handler(404)
async def not_found(request: Request, exc: HTTPException):
return templates.TemplateResponse("404.html", {"request": request}, status_code=404)
创建templates/404.html。
4.10 静态文件的高级配置:缓存、压缩
在生产环境,应设置缓存头。可以通过自定义StaticFiles子类或使用中间件。例如:
from starlette.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000)
另外,可以使用WhiteNoise等库提供更好的静态文件服务,但通常不必要。
5. 重点知识点总结
核心语法速查表
| 功能 | 代码 |
|---|---|
| 挂载静态文件目录 | app.mount("/static", StaticFiles(directory="static"), name="static") |
| 初始化Jinja2模板 | templates = Jinja2Templates(directory="templates") |
| 渲染模板并返回 | return templates.TemplateResponse("index.html", {"request": request, "key": value}) |
| 模板变量输出 | {{ variable }} |
| 模板循环 | {% for item in items %}{{ item }}{% endfor %} |
| 模板条件 | {% if condition %}...{% endif %} |
| 模板继承 | {% extends "base.html" %} |
| 定义块 | {% block content %}{% endblock %} |
| 包含子模板 | {% include "header.html" %} |
易错点与避坑指南
-
❌ 忘记传递
request参数给模板
Jinja2Templates要求上下文中必须包含request,否则会报错。 -
❌ 模板目录路径错误
确保Jinja2Templates(directory="templates")中的路径是相对于当前工作目录或绝对路径。建议使用Path(__file__).parent.parent / "templates"等动态路径。 -
❌ 静态文件无法访问
检查挂载的directory路径是否正确,并且文件确实存在于该目录。另外,静态文件路由必须在其他路由之前挂载,但顺序一般不严格要求。 -
❌ 模板中访问API端点时出现端口/主机问题
在模板中生成绝对URL时,应使用request.url_for或配置base_url。避免硬编码localhost:8000。 -
❌ 在生产环境中暴露
uploads目录
上传目录如果直接挂载为静态,可能会被恶意用户直接访问所有上传文件(包括敏感文件)。建议通过额外的路由进行权限控制。 -
❌ 模板中使用了HTML标签但被转义
如果需要输出HTML片段,使用{{ html_content | safe }},但要确保内容可信,防止XSS攻击。
最佳实践
- 模板继承:使用基础模板
base.html,避免重复代码。 - 静态文件版本控制:添加查询参数(如
?v=1.0)或在文件名中嵌入哈希,避免浏览器缓存。 - 生产环境分离静态文件:使用Nginx或CDN提供服务,减轻FastAPI压力。
- 使用
request.url_for构建动态URL:便于路由变更。 - 模板中仅包含展示逻辑,不在模板中执行复杂计算。
- 对用户上传的文件进行隔离:为不同用户创建子目录,并在访问时鉴权。
6. 课后作业 & 思考题
实操练习题(必做)
-
基础练习:创建一个关于页面
- 路由
/about,使用模板渲染,展示课程介绍和作者信息 - 继承
base.html,修改标题和内容
- 路由
-
进阶练习:用户注册表单页面
- 创建
/register页面(GET方法显示表单) - 创建
/registerPOST方法接收表单数据(用户名、邮箱、密码),存储到内存字典,并返回成功页面 - 使用模板显示表单,并实现简单的客户端验证(HTML5 required)
- 创建
-
挑战练习:文件上传画廊
- 实现一个页面
/gallery,展示uploads目录下所有图片的缩略图 - 提供上传表单,上传新图片后自动刷新画廊
- 注意:需要遍历目录获取文件列表,并在模板中生成图片标签
- 实现一个页面
理论思考题
- 为什么FastAPI不内置模板引擎,而是推荐Jinja2? 这种设计带来哪些好处?
- 静态文件托管在生产环境下,使用
StaticFiles与使用Nginx相比有何优劣? - Jinja2模板中的
autoescape机制是什么?如何防止XSS攻击? - 在混合架构中,如何实现API与模板页面的统一认证?(提示:依赖注入)
拓展学习方向
- 学习Jinja2高级特性:宏(macro)、过滤器自定义、全局函数注入
- 使用
fastapi-admin:快速生成后台管理界面 - 集成前端框架:在静态文件中使用React/Vue,通过API交互
- 邮件模板:使用Jinja2生成HTML邮件内容
7. 本节干货总结
核心考点(面试/自测)
-
FastAPI中如何挂载静态文件目录?
app.mount("/static", StaticFiles(directory="static"), name="static") -
如何在FastAPI中使用Jinja2模板?
创建Jinja2Templates实例,在路由中调用templates.TemplateResponse。 -
模板渲染时必须传递的参数是什么?
request对象,因为模板需要它生成URL等。 -
Jinja2模板中的
{% block %}有什么作用?
定义可被子模板覆盖的区域,用于模板继承。 -
如何防止模板中的XSS攻击?
默认Jinja2自动转义HTML特殊字符,使用safe过滤器时需确保内容可信。
实际工作应用场景
-
场景1:内部管理系统
企业内部使用的数据看板、配置管理界面,无需复杂前端框架,用模板快速搭建。 -
场景2:文件上传服务的前端展示
提供简单的上传页面和文件列表,方便非技术人员使用。 -
场景3:邮件模板渲染
生成HTML邮件内容,发送给用户(如注册激活、密码重置)。 -
场景4:API测试沙箱
提供一个友好的网页界面,允许用户在页面上测试API,展示请求和响应。 -
场景5:静态站点生成辅助
结合模板和数据,生成静态HTML文件,部署到CDN。
下节课预告
本节课我们给FastAPI项目添加了前端展示能力。现在,我们的应用既能提供API,又能提供HTML页面。但在整个应用的结构中,我们总是需要在不同路由中重复编写数据库连接、认证逻辑等。FastAPI提供了一个优雅的解决方案——依赖注入(Dependency Injection),它可以让我们轻松复用公共逻辑,减少重复代码,提高可测试性。
下节课(第10课),我们将学习:
- 依赖注入的原理:
Depends如何工作 - 局部依赖:为单个路由注入依赖
- 全局依赖:为整个应用或路由组统一注入
- 依赖的依赖:嵌套依赖
- 实战:数据库会话管理、认证依赖、分页参数依赖等
依赖注入是FastAPI高级特性的基础,也是实现可维护大型项目的关键。下节课见!
🔗《20节课 FastAPI 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

596

被折叠的 条评论
为什么被折叠?



