第9课:FastAPI|静态文件托管|Jinja2模板引擎配置与页面渲染

在这里插入图片描述


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模板语法(变量输出、循环、条件,本节会讲解)

学完能掌握什么

学完本节课后,你将具备以下能力:

  1. 配置静态文件服务:让FastAPI应用能够对外提供图片、样式表、JavaScript文件
  2. 使用Jinja2渲染HTML:生成动态页面,并利用模板继承提高复用性
  3. 构建简单的全栈功能:例如文件上传后的展示页面、数据可视化报表页面
  4. 理解API与模板混合模式:在同一个项目中,部分路由返回JSON,部分返回HTML
  5. 为后续课程打基础:某些后台管理界面或第三方登录回调页面可能需要模板

适用人群

  • 需要用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使用相同的模板引擎,语法强大且易用。

工作流程

  1. 后端路由函数准备数据(字典)。
  2. 调用TemplateResponse,传入模板文件名和上下文数据。
  3. Jinja2加载模板文件,解析变量、循环、条件等,渲染为HTML字符串。
  4. 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" %}

易错点与避坑指南

  1. ❌ 忘记传递request参数给模板
    Jinja2Templates要求上下文中必须包含request,否则会报错。

  2. ❌ 模板目录路径错误
    确保Jinja2Templates(directory="templates")中的路径是相对于当前工作目录或绝对路径。建议使用Path(__file__).parent.parent / "templates"等动态路径。

  3. ❌ 静态文件无法访问
    检查挂载的directory路径是否正确,并且文件确实存在于该目录。另外,静态文件路由必须在其他路由之前挂载,但顺序一般不严格要求。

  4. ❌ 模板中访问API端点时出现端口/主机问题
    在模板中生成绝对URL时,应使用request.url_for或配置base_url。避免硬编码localhost:8000

  5. ❌ 在生产环境中暴露uploads目录
    上传目录如果直接挂载为静态,可能会被恶意用户直接访问所有上传文件(包括敏感文件)。建议通过额外的路由进行权限控制。

  6. ❌ 模板中使用了HTML标签但被转义
    如果需要输出HTML片段,使用{{ html_content | safe }},但要确保内容可信,防止XSS攻击。

最佳实践

  1. 模板继承:使用基础模板base.html,避免重复代码。
  2. 静态文件版本控制:添加查询参数(如?v=1.0)或在文件名中嵌入哈希,避免浏览器缓存。
  3. 生产环境分离静态文件:使用Nginx或CDN提供服务,减轻FastAPI压力。
  4. 使用request.url_for构建动态URL:便于路由变更。
  5. 模板中仅包含展示逻辑,不在模板中执行复杂计算
  6. 对用户上传的文件进行隔离:为不同用户创建子目录,并在访问时鉴权。

6. 课后作业 & 思考题

实操练习题(必做)

  1. 基础练习:创建一个关于页面

    • 路由/about,使用模板渲染,展示课程介绍和作者信息
    • 继承base.html,修改标题和内容
  2. 进阶练习:用户注册表单页面

    • 创建/register页面(GET方法显示表单)
    • 创建/register POST方法接收表单数据(用户名、邮箱、密码),存储到内存字典,并返回成功页面
    • 使用模板显示表单,并实现简单的客户端验证(HTML5 required)
  3. 挑战练习:文件上传画廊

    • 实现一个页面/gallery,展示uploads目录下所有图片的缩略图
    • 提供上传表单,上传新图片后自动刷新画廊
    • 注意:需要遍历目录获取文件列表,并在模板中生成图片标签

理论思考题

  1. 为什么FastAPI不内置模板引擎,而是推荐Jinja2? 这种设计带来哪些好处?
  2. 静态文件托管在生产环境下,使用StaticFiles与使用Nginx相比有何优劣?
  3. Jinja2模板中的autoescape机制是什么?如何防止XSS攻击?
  4. 在混合架构中,如何实现API与模板页面的统一认证?(提示:依赖注入)

拓展学习方向

  • 学习Jinja2高级特性:宏(macro)、过滤器自定义、全局函数注入
  • 使用fastapi-admin:快速生成后台管理界面
  • 集成前端框架:在静态文件中使用React/Vue,通过API交互
  • 邮件模板:使用Jinja2生成HTML邮件内容

7. 本节干货总结

核心考点(面试/自测)

  1. FastAPI中如何挂载静态文件目录?
    app.mount("/static", StaticFiles(directory="static"), name="static")

  2. 如何在FastAPI中使用Jinja2模板?
    创建Jinja2Templates实例,在路由中调用templates.TemplateResponse

  3. 模板渲染时必须传递的参数是什么?
    request对象,因为模板需要它生成URL等。

  4. Jinja2模板中的{% block %}有什么作用?
    定义可被子模板覆盖的区域,用于模板继承。

  5. 如何防止模板中的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 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值