FastAPI 静态文件
FastAPI 静态文件学习笔记
一、基本用法 —StaticFiles
1. 挂载静态文件目录
fromfastapiimportFastAPIfromfastapi.staticfilesimportStaticFiles app=FastAPI()# 将 ./static 目录挂载到 /static 路径app.mount("/static",StaticFiles(directory="static"),name="static")目录结构:
project/ ├── main.py └── static/ ├── css/ │ └── style.css ├── js/ │ └── app.js └── images/ └── logo.png访问方式:
http://localhost:8000/static/css/style.css http://localhost:8000/static/js/app.js http://localhost:8000/static/images/logo.png2. 参数说明
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
directory | str/Path | 必填 | 静态文件目录路径 |
html | bool | False | 是否启用 HTML 模式 |
check_dir | bool | True | 启动时检查目录是否存在 |
二、HTML 模式
1. 启用 HTML 模式
app.mount("/",StaticFiles(directory="static",html=True),name="static")HTML 模式行为:
| 请求路径 | 文件存在情况 | 响应 |
|---|---|---|
/ | index.html存在 | 返回index.html |
/about | about.html存在 | 返回about.html |
/about/ | about/index.html存在 | 返回about/index.html |
/style.css | style.css存在 | 返回style.css |
/missing | 无匹配文件 | 404 |
2. 典型用途 — 托管 SPA 前端
# Vue / React 构建产物app.mount("/",StaticFiles(directory="dist",html=True),name="spa")dist/ ├── index.html ├── assets/ │ ├── index.abc123.js │ └── index.def456.css └── favicon.ico注意:SPA 的前端路由(如
/about、/user/1)需要服务端统一返回index.html,html=True只处理.html文件匹配,不支持 SPA fallback。完整 SPA 托管方案见第五节。
三、挂载位置与路由优先级
1.app.mount的特性
mount创建的是一个子应用,会匹配该路径下的所有请求,不再经过主应用的路由匹配:
app=FastAPI()@app.get("/static/data")# ← 永远不会被访问到asyncdefget_data():return{"data":"hello"}app.mount("/static",StaticFiles(directory="static"),name="static")# /static/* 的所有请求都被 StaticFiles 拦截2. 正确的挂载顺序
app=FastAPI()# ① 先注册 API 路由@app.get("/api/data")asyncdefget_data():return{"data":"hello"}# ② 最后挂载静态文件(避免覆盖 API 路由)app.mount("/static",StaticFiles(directory="static"),name="static")3. 挂载多个静态目录
app.mount("/static",StaticFiles(directory="static"),name="static")app.mount("/uploads",StaticFiles(directory="uploads"),name="uploads")app.mount("/assets",StaticFiles(directory="assets"),name="assets")四、在 Jinja2 模板中引用静态文件
fromfastapiimportFastAPI,Requestfromfastapi.staticfilesimportStaticFilesfromfastapi.templatingimportJinja2Templates app=FastAPI()app.mount("/static",StaticFiles(directory="static"),name="static")templates=Jinja2Templates(directory="templates")@app.get("/page")asyncdefpage(request:Request):returntemplates.TemplateResponse("page.html",{"request":request})模板中引用:
<!-- 使用 url_for 动态生成路径 --><linkrel="stylesheet"href="{{ url_for('static', path='/css/style.css') }}"><scriptsrc="{{ url_for('static', path='/js/app.js') }}"></script><imgsrc="{{ url_for('static', path='/images/logo.png') }}"alt="logo">
url_for的第一个参数是mount时的name,第二个参数path是文件在静态目录中的相对路径。
五、SPA 前端托管完整方案
Vue / React 等单页应用需要所有未匹配路径返回index.html:
方案一:中间件 fallback
fromfastapiimportFastAPI,Requestfromfastapi.staticfilesimportStaticFilesfromfastapi.responsesimportFileResponse app=FastAPI()# API 路由@app.get("/api/data")asyncdefget_data():return{"data":"hello"}# 静态资源(js/css/images)app.mount("/assets",StaticFiles(directory="dist/assets"),name="assets")# SPA fallback:未匹配的路由返回 index.html@app.get("/{full_path:path}")asyncdefserve_spa(request:Request,full_path:str):returnFileResponse("dist/index.html")方案二:自定义静态文件中间件
importosfromfastapiimportFastAPI,Requestfromfastapi.responsesimportFileResponse,Responsefromstarlette.middleware.baseimportBaseHTTPMiddleware app=FastAPI()DIST_DIR="dist"classSPAMiddleware(BaseHTTPMiddleware):asyncdefdispatch(self,request:Request,call_next):response=awaitcall_next(request)# 如果路由返回 404 且是 HTML 请求,返回 index.htmlifresponse.status_code==404:accept=request.headers.get("accept","")if"text/html"inaccept:index_path=os.path.join(DIST_DIR,"index.html")ifos.path.exists(index_path):returnFileResponse(index_path)returnresponse app.add_middleware(SPAMiddleware)app.mount("/assets",StaticFiles(directory=f"{DIST_DIR}/assets"),name="assets")方案三:纯静态挂载(最简单)
app=FastAPI()# API 路由@app.get("/api/data")asyncdefget_data():return{"data":"hello"}# 静态文件(html=True 自动处理 index.html)app.mount("/",StaticFiles(directory="dist",html=True),name="spa")局限:
html=True只在请求路径对应.html文件存在时返回,不支持 SPA 的动态路由(如/user/123)。
六、文件上传与静态文件服务结合
importosimportuuidfromfastapiimportFastAPI,UploadFile,Filefromfastapi.staticfilesimportStaticFiles app=FastAPI()UPLOAD_DIR="uploads"os.makedirs(UPLOAD_DIR,exist_ok=True)@app.post("/upload")asyncdefupload_file(file:UploadFile=File(...)):# 生成唯一文件名ext=os.path.splitext(file.filename)[1]filename=f"{uuid.uuid4().hex}{ext}"filepath=os.path.join(UPLOAD_DIR,filename)# 保存文件withopen(filepath,"wb")asf:content=awaitfile.read()f.write(content)return{"filename":filename,"url":f"/uploads/{filename}",}# 挂载上传目录app.mount("/uploads",StaticFiles(directory=UPLOAD_DIR),name="uploads")七、生产环境注意事项
1. Nginx 反向代理直接服务静态文件
生产环境建议由 Nginx 直接处理静态文件,性能更好:
server { listen 80; server_name example.com; # Nginx 直接服务静态文件 location /static/ { alias /var/www/static/; expires 30d; add_header Cache-Control "public, immutable"; } # API 请求转发给 FastAPI location /api/ { proxy_pass http://127.0.0.1:8000; } }2. 缓存控制
fromstarlette.responsesimportResponsefromfastapi.staticfilesimportStaticFilesclassCachedStaticFiles(StaticFiles):asyncdeflookup_path(self,path:str):full_path,stat_result=awaitsuper().lookup_path(path)ifstat_result:# 根据文件扩展名设置缓存ifpath.endswith((".js",".css",".woff2",".png",".jpg")):self.headers["Cache-Control"]="public, max-age=31536000, immutable"else:self.headers["Cache-Control"]="public, max-age=3600"returnfull_path,stat_result app.mount("/static",CachedStaticFiles(directory="static"),name="static")3. 目录安全
| 风险 | 防护措施 |
|---|---|
| 目录遍历攻击 | StaticFiles默认禁止..路径 |
| 敏感文件泄露 | 不要将配置文件、.env放在静态目录 |
| 上传恶意文件 | 限制文件类型、重命名文件、隔离上传目录 |
| 大文件消耗带宽 | Nginx 层限制请求体大小 |
八、StaticFilesvs 其他方案
| 方案 | 适用场景 | 性能 | 灵活性 |
|---|---|---|---|
StaticFiles | 开发环境、小型项目 | 中 | 低 |
| Nginx 直接服务 | 生产环境 | 高 | 高 |
| CDN | 全球分发、高流量 | 最高 | 最高 |
| 云存储(OSS/S3) | 用户上传文件、海量存储 | 高 | 高 |
推荐:
开发环境 → StaticFiles(零配置) 生产环境 → Nginx 直接服务静态文件 + CDN 加速 用户上传 → 云存储(OSS / S3)+ CDN九、完整示例
fromfastapiimportFastAPI,Request,UploadFile,Filefromfastapi.staticfilesimportStaticFilesfromfastapi.responsesimportFileResponseimportos,uuid app=FastAPI()# ---- 目录准备 ----os.makedirs("static/css",exist_ok=True)os.makedirs("static/js",exist_ok=True)os.makedirs("uploads",exist_ok=True)# ---- API 路由 ----@app.get("/api/hello")asyncdefhello():return{"message":"Hello, FastAPI!"}@app.post("/api/upload")asyncdefupload(file:UploadFile=File(...)):ext=os.path.splitext(file.filename)[1]filename=f"{uuid.uuid4().hex}{ext}"withopen(f"uploads/{filename}","wb")asf:f.write(awaitfile.read())return{"url":f"/uploads/{filename}"}# ---- 静态文件挂载 ----app.mount("/static",StaticFiles(directory="static"),name="static")app.mount("/uploads",StaticFiles(directory="uploads"),name="uploads")# ---- SPA fallback(放最后)----@app.get("/{full_path:path}")asyncdefspa_fallback(request:Request,full_path:str):returnFileResponse("static/index.html")十、注意事项
mount必须放在路由注册之后:否则会拦截所有匹配路径的请求,导致 API 路由不可达。- 目录必须存在:默认
check_dir=True,目录不存在会抛异常。启动前确保目录已创建。 name参数的作用:用于url_for反向生成 URL,建议始终设置。- 开发 vs 生产:
StaticFiles适合开发,生产环境推荐 Nginx / CDN。 - 路径以
/结尾:访问目录路径时,StaticFiles会自动查找index.html(需html=True)。
