静态资源服务的核心问题不是"能不能服务",而是"该由哪一层服务":FastAPI 的 StaticFiles 一行挂载即可让应用进程直接吐文件,方便但占用应用资源且缓存控制简陋;生产架构里 Nginx 或 CDN 才是静态资源的主力,应用层只保留接口职责。本节对比两种方案,顺带配好 Jinja2 模板与文档页静态资源。
阅读完本节,你应当能够:
from fastapi.staticfiles import StaticFiles app.mount("/static", StaticFiles(directory="static"), name="static")
mount 把一个子应用挂到路径前缀上——StaticFiles 是 Starlette 提供的 ASGI 子应用,负责目录映射、ETag、基本的内容类型判断。挂载后,浏览器请求静态前缀下的任何路径都由它响应,不再经过路由系统。
开发期这是完美方案:一条命令、零配置、与热重载共存。Jinja2 模板渲染通常与它配套:
from fastapi.templating import Jinja2Templates templates = Jinja2Templates(directory="templates") @app.get("/page", response_class=HTMLResponse) def page(request: Request): return templates.TemplateResponse( request=request, name="page.html", context={"title": "控制台", "items": load_items()}, )
值得注意 API 的历史差异:TemplateResponse 的参数形态在 Starlette 新版本中把 request 提升为关键字参数(旧教程里"context 内嵌 request"的写法已弃用)——读旧示例代码时留意这一处版本签名差异,报"dict 没有 url 属性"基本就是新旧写法混用。这套组合能让 FastAPI 承担完整的传统服务端渲染站点(管理后台、内部工具),但也要诚实面对定位:没有 Django 那套表单、admin、消息框架的配套,页面复杂起来后每个能力都要手拼。FastAPI 做页面不是不行,是性价比问题——后台 API 加独立前端应用,通常比在 FastAPI 里堆模板更清晰。
进入生产,静态资源的合理架构是三级漏斗。最外层 CDN:地理位置就近、带宽不受应用集群限制,公开站点的静态资源大头应该终结在这里。中间层 Nginx 反向代理:location 规则把静态路径直接映射到磁盘目录或 CDN 回源,绕过应用进程——Nginx 发静态文件的效率(sendfile 零拷贝、事件驱动)远高于 Python 进程,且缓存头、压缩、断点续传的控制项齐全。最内层才是 应用进程:只服务必须动态生成的内容。
对照表:
| 维度 | StaticFiles 应用内 | Nginx 直出 | CDN |
|---|---|---|---|
| 吞吐 | 受 worker 数与 GIL 限制 | 高(零拷贝) | 最高(边缘节点) |
| 缓存控制 | 基础 ETag | 完整配置 | 完整加回源策略 |
| 运维复杂度 | 零 | 一份配置 | 计费与域名接入 |
| 适用阶段 | 开发与小流量 | 生产默认 | 公开大流量 |
迁移路径平滑:开发用 StaticFiles,上线时 Nginx 加一条静态 location 即可,应用代码不动——因为挂载路径与 Nginx 路径可以设计得一致。前提是团队在一开始就约定静态资源统一走静态前缀,混在业务路径下的静态请求会让分层服务变得困难。
静态资源的服务端配置只有一半学问,另一半在发布策略。浏览器缓存依赖响应头:Cache-Control 的 max-age 决定缓存期,immutable 配合内容寻址文件名是黄金组合。内容寻址(文件名里带内容哈希,比如带指纹的 bundle 名)保证内容一变名字就变——旧缓存自然失效,新文件全新拉取,不存在"用户看到旧版页面配新版接口"的经典事故。
FastAPI 侧不需要操心这些(那是构建工具与 Nginx 的事),但要避免一个反向错误:把带哈希的静态资源配置成短缓存或 no-cache——白白放弃浏览器缓存,移动端弱网用户每次全量拉取。文档页的静态资源(Swagger UI 自己的 JS)由框架处理,通常无感;只有当你把 docs 关掉改用内部托管时,才会接触到这层细节。
三类例外不交给外层:运行时生成的文件(用户上传后立即的回显、动态生成的二维码)生成期由应用处理,落地后转对象存储加 CDN;带鉴权的文件下发——下载权限要校验用户身份,Nginx 的静态直出没有这个逻辑,方案是应用校验后签发短期有效的直链(对象存储的预签名 URL 模式)或应用用 FileResponse 走进程内下发(第 4 章的 FileResponse 支持异步读盘,中小文件可接受);服务端推送的流式内容(SSE、WebSocket)本质不是静态资源,但常被误配到静态层——它们必须终结在应用进程。
⚠️ 常见坑:目录遍历攻击。StaticFiles 内部做了路径规范化,直接用它挂载是安全的;风险出现在"自己写文件下载接口拼接路径"的场景——用户传来的文件名直接拼进磁盘路径,构造向上跳层的路径即可读到任意文件。自写下载逻辑必须白名单化文件名或用 pathlib 校验解析后的路径仍在目标目录内。这个坑与框架无关,但静态文件话题下发生率最高。
一个十分钟的实验能直观建立"应用层对比代理层"的性能直觉。第一步,只挂 StaticFiles,用并发工具压一个两兆的静态文件,记录吞吐与 Python 进程的 CPU 占用。第二步,装一个本地 Nginx,配置静态路径直出同目录文件,同样压测对比——吞吐通常高出一个量级,而 Python 进程完全空闲。第三步,在 Nginx 配置里加 expires 头与 gzip,用浏览器开发者工具观察第二次请求的缓存命中与传输体积的变化。
三步做完,第 4 章 GZip 的取舍、本章的三级漏斗、缓存头的作用就都不再是文字——你亲眼看到同一份文件在两层服务下的代价差异,以及一行配置换来的缓存收益。这个实验也是说服团队"把静态资源迁出应用进程"的最快方式:数据比架构图有说服力。
顺带一提开发期的对照结论:本地开发时 StaticFiles 的性能完全够用(单人流量),因此不必在开发环境模拟 Nginx——分层带来的环境差异靠"路径约定一致"抹平(前文强调过的静态前缀统一),这是"开发生产一致性"在静态资源上的具体实践。

**问题:单页应用(前端打包产物)怎么部署?**本章问题的最高频变体。正确形态:构建产物交给 Nginx 直出,应用只提供接口;Nginx 配置里前端路由回退(所有非静态路径返回入口页)让前端路由接管。错误形态:把打包产物挂进应用的 StaticFiles 让应用进程服务全部静态请求——开发期可以这么偷懒,上量后就是性能债。
**问题:用户上传的文件存哪?**小规模直接本地目录加 StaticFiles(注意上传目录与代码目录分离、文件名重新生成);规模化走对象存储——应用收文件、转存对象存储、库里记地址,访问经存储的 CDN 域名。上传的校验三件套(类型、大小、重命名)在正文安全清单里,此处补第四件:唯一文件名天然规避并发同名冲突。
**问题:开发用应用挂载、生产用 Nginx,路径会不一致吗?**不会,只要遵守静态前缀统一的约定(正文强调过)。两层的磁盘目录可以不同(Nginx 映射到发布目录),但 URL 空间一致——URL 是契约,磁盘布局是实现,实现的差异不该泄漏到契约层。这也是把路径约定写进团队规范的又一个理由。
**问题:媒体文件要做缩略图、转码这类处理怎么办?**那是计算任务不是静态服务——上传后触发处理任务(第 5 章的队列),完成后产物入静态资源层。处理中的占位、失败的兜底属于业务逻辑,与应用层的边界清晰可辨。
集成层理清,第 7 章收官:部署形态、性能调优、项目结构与继续学习的路线图。
静态资源话题的边界与延伸。边界一:本节的方案以"静态内容可提前构建"为前提——服务端动态渲染的页面(每次请求都不同的 HTML)不属于静态资源,它们的性能优化属于缓存策略与模板效率的领地。边界二:国内环境的内容分发有额外考量(备案、加速节点分布),CDN 选型时这些现实约束常常压倒纯技术参数——工程决策从不只在技术维度做。
延伸两条。其一,渐进式前端架构的中间态:微前端或多端共用后端时,静态资源的版本协调(哪个前端版本配哪个后端版本)需要发布约定支撑,规范文件里的版本标记加网关的路由规则是轻量解。其二,安全头(内容安全策略、严格传输安全)的注入位置——多数安全头适合在反向代理层统一加(一处配置全站生效),个别需要应用层参与的(带随机值的策略头)走中间件——又是"哪层看得见所需信息"母题的一次复现。静态资源看似简单,它的每个决策点都是架构分层思想的练习题。
补一个中小团队最常见的折中形态作为收尾:单台服务器跑 Nginx 加应用——Nginx 同时承担静态直出、TLS 终止与反向代理,应用容器在回环地址监听。这个形态没有 CDN 与集群,但第 6.3 节的三级漏斗它占了中间一级,静态资源与动态请求已经分层,日后加 CDN 只是多一层指向,无需改动应用。它的配置量是一份十行的 Nginx 配置——多数项目从这个形态起步,也应该从这个形态起步:分层的思想先落地,规模的东西后添加,顺序反了就是为不存在的流量提前付费。