概述

本文档说明如何将前端单页应用(SPA)接入到本 FastAPI 后端项目中。后端已经内置了 SPA 支持,可以无缝处理前端路由和静态资源。

目录结构约定

后端期望前端构建产物位于 src/static 目录:

project-tianlu/
├── src/
│   ├── app.py
│   ├── static/        # 前端构建产物目录(由前端构建生成)
│   │   ├── index.html
│   │   ├── assets/
│   │   └── ...
│   └── ...
└── ...

重要说明

  • src/static/index.html:前端 SPA 的入口文件,必须存在

  • 其他静态资源(JS/CSS/图片等)也都在 src/static 目录下

FastAPI 后端配置

后端在 src/app.py 中已经内置了 SPA 支持:

from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHTTPException
class SPAStaticFiles(StaticFiles):
    async def get_response(self, path: str, scope):
        try:
            return await super().get_response(path, scope)
        except StarletteHTTPException as ex:
            if ex.status_code == 404:
                return await super().get_response("index.html", scope)
            else:
                raise ex

# 挂载 SPA 静态资源
app.mount("/", SPAStaticFiles(directory="src/static", html=True), name="app")

如果没有这段配置会有什么问题?

  • 缺少 SPAStaticFiles 的 404 回退逻辑时

    • 直接访问或刷新诸如 /report/list/dashboard 等前端路由时,请求会返回 后端的 404 JSON,而不是前端页面。

    • 因为默认 StaticFiles 找不到对应的物理文件(例如 /report/list),就直接抛出 404,不会再回退到 index.html 交给前端路由处理,SPA 的「前端路由 + 深链接」场景就会全部失效。

  • 总结

    • SPAStaticFiles 负责「把所有未知路径统一回退到 index.html」,这是 SPA 正常支持浏览器刷新、深链接(直接访问任意前端路由)的关键。

    • app.mount("/", ...) 则负责「把前端静态资源挂到后端根路径」,否则后端根路径下根本没有前端页面可用。

工作原理

  • 所有非 /api 开头的请求,都会优先按静态文件处理

  • 当找不到对应静态文件(404)时,统一返回 src/static/index.html,交由前端路由处理(典型 SPA 模式)

前端项目接入步骤

1. 配置前端构建输出目录为 src/static

Vue (Vite) 项目

vite.config.ts 中配置:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    outDir: '../src/static',  // 输出到后端项目的 src/static 目录
  },
  // 如果后端挂载在根路径 `/`,base 保持默认即可
  // base: '/',
})

React (Vite) 项目

vite.config.ts 中配置:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: '../src/static',  // 输出到后端项目的 src/static 目录
  },
})

Vue CLI 项目

vue.config.js 中配置:

module.exports = {
  outputDir: '../src/static',  // 输出到后端项目的 src/static 目录
}

Create React App (CRA)

CRA 默认输出目录就是 build,需要修改为 src/static

package.json 中修改构建脚本:

{
  "scripts": {
    "build": "react-scripts build && mv build ../src/static"
  }
}

或者使用 react-app-rewired 自定义配置,设置 outputDir: '../src/static'

2. 打包前端项目

在前端项目根目录执行:

# 安装依赖(如果还没有安装)
npm install

# 构建生产版本
npm run build

构建完成后,前端构建产物会直接输出到 src/static 目录。

3. 启动后端服务

在后端项目根目录执行:

# 使用 uv 运行
uv run uvicorn src.app:app --reload

# 或者使用其他方式
python -m uvicorn src.app:app --reload

打开浏览器访问:http://127.0.0.1:8000,应该能看到前端 SPA 页面。

4. API 调用说明

后端所有 API 路径统一挂载在 /api 前缀,例如:

  • GET /api/test_report/...

  • POST /api/project/...

  • GET /api/auth/...

前端调用时统一以 /api 开头发请求,确保与静态资源路由不冲突。

前端 API 调用示例

// 使用 fetch
const response = await fetch('/api/test_report/list')
const data = await response.json()

// 使用 axios
import axios from 'axios'
const response = await axios.get('/api/test_report/list')

开发模式推荐

方式 A:前后端分离开发(推荐)

优点:前端热更新,开发效率高

  1. 前端开发服务器(前端项目目录):

    npm run dev
    # 通常运行在 http://localhost:5173 (Vite)
    
  2. 配置前端代理(Vite 示例):

    vite.config.ts 中配置:

    export default defineConfig({
      server: {
        proxy: {
          '/api': {
            target: 'http://127.0.0.1:8000',
            changeOrigin: true,
          },
        },
      },
    })
    
  3. 后端开发服务器(后端项目目录):

    uv run uvicorn src.app:app --reload
    
  4. 访问方式

    • 前端页面:http://localhost:5173

    • API 请求会自动代理到 http://127.0.0.1:8000/api

方式 B:统一通过 FastAPI 访问

优点:更接近生产环境

  1. 每次修改前端后执行 npm run build

  2. 构建产物会自动输出到 src/static 目录

  3. 启动后端服务,访问 http://127.0.0.1:8000

适用场景:前端改动不频繁,主要调试后端接口时使用

路由优先级说明

FastAPI 的路由匹配顺序很重要:

# 1. 先注册 API 路由(必须在静态文件之前)
app.include_router(src.auth.router.router, prefix="/api")
app.include_router(src.test_report.router.router, prefix="/api")
# ... 其他 API 路由

# 2. 最后挂载静态文件服务
app.mount("/", SPAStaticFiles(directory="src/static", html=True), name="app")

匹配规则

  • /api 开头的请求 → 匹配 API 路由 → 返回 JSON 响应

  • 其他请求 → 尝试静态文件 → 找不到则返回 index.html

生产环境部署建议

1. 使用 Nginx 反向代理(推荐)

生产环境建议使用 Nginx 处理静态文件,FastAPI 只处理 API 请求:

server {
    listen 80;
    server_name your-domain.com;

    # 静态文件由 Nginx 直接提供
    location / {
        root /path/to/project/src/static;
        try_files $uri $uri/ /index.html;
    }

    # API 请求转发到 FastAPI
    location /api {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

2. 使用 Gunicorn + Uvicorn Workers

gunicorn src.app:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

3. 使用 Docker 部署

可以创建一个包含前端构建产物和后端代码的 Docker 镜像,统一部署。

总结

  • ✅ 前端构建产物放在 src/static 目录

  • ✅ API 路由统一使用 /api 前缀

  • ✅ API 路由必须在静态文件挂载之前注册

  • ✅ 开发时推荐前后端分离,生产时统一部署

  • ✅ 生产环境建议使用 Nginx 处理静态文件

按照以上步骤配置,即可将前端 SPA 无缝接入到 FastAPI 后端中。