一个端口搞定前后端:Next.js 静态导出 + FastAPI 单服务部署

本文最后更新于 2026年7月21日 凌晨

前后端分离项目最烦的事情之一,就是部署的时候要开两个端口。前端一个、后端一个,跨域、代理、CORS、WebSocket 一堆问题全冒出来了。最近在一个仓储管理系统项目上,我们从双端口架构迁移到单端口,一个改动连带解决了五个问题。

为什么要折腾这件事

项目用的是 Next.js 15 + FastAPI 的技术栈。原来的部署方式很标准:

  • FastAPI 跑在 :8000 提供 API
  • Next.js 用 next start 跑在 :8300 提供前端
  • 前端通过 rewrites/api/* 代理到后端

看起来没问题,但实际跑起来一堆毛病。用户从局域网另一台机器访问前端地址时:

  • 首页卡在 Loading,数据永远加载不出来
  • 浏览器控制台报 WebSocket HMR 连接失败
  • 字体文件 403 Forbidden
  • 移动端菜单打不开
  • PC 端侧边栏子菜单不可见

每个问题单独看都像是前端 Bug,但逐个排查后发现,根因都指向同一个架构问题——双端口部署

关键发现:代码早就写好了,只是没接上

在翻后端 main.py 的时候,发现之前开发者已经写了静态文件服务的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SPAStaticFiles(StaticFiles):
"""前端路由刷新时返回 index.html"""
async def get_response(self, path: str, scope):
if path.startswith("api/") or path.startswith("docs"):
raise StarletteHTTPException(status_code=404)
try:
return await super().get_response(path, scope)
except StarletteHTTPException as exc:
if exc.status_code != 404:
raise
return await super().get_response("index.html", scope)

if FRONTEND_DIST_DIR.exists():
app.mount("/", SPAStaticFiles(directory=str(FRONTEND_DIST_DIR)), name="frontend")

这段代码的逻辑完全正确:API 请求和静态文件请求分流,前端路由 404 时 fallback 到 index.html(SPA 刷新支持)。

但它从来没生效过,因为两个问题:

  1. 路径不对FRONTEND_DIST_DIR 指向 frontend/dist,但 Next.js 默认输出到 .next/ 目录,根本没有 dist/
  2. Next.js 不是纯静态应用:默认 SSR 模式下,.next/ 目录里是运行时文件,不是纯静态 HTML/JS/CSS,FastAPI 无法直接托管

解决方案:两行改动

改动一:Next.js 开启静态导出

next.config.ts 改两行:

1
2
3
4
const nextConfig: NextConfig = {
output: 'export', // 静态导出模式
images: { unoptimized: true }, // export 模式必须关闭图片优化
};

output: 'export' 是 Next.js 的静态导出模式。执行 npm run build 后,所有页面会被预渲染成静态 HTML/JS/CSS 文件,输出到 out/ 目录。所有路由页面全部变成纯静态文件,不再需要 Node.js 运行时。

注意:静态导出模式下 rewritesredirectsheaders 等服务器端功能自动失效,这正是我们想要的——生产环境不需要代理层,因为前后端已经跑在同一个端口上了。开发模式下 rewrites 仍然在 next dev 中正常工作。

改动二:修正 FastAPI 静态文件路径

项目实际的前端目录是 frontend-shadcn-ui(改前路径指向的 frontend/dist 既不存在、命名也与实际不符)。

1
2
3
4
5
# 改前:指向不存在且命名不符的目录
FRONTEND_DIST_DIR = Path(__file__).parent.parent.parent / "frontend" / "dist"

# 改后:指向 Next.js 静态导出目录
FRONTEND_DIST_DIR = Path(__file__).parent.parent.parent / "frontend-shadcn-ui" / "out"

就这样。构建前端 → 重启后端 → 一个端口同时服务 API 和前端。

构建和部署流程

1
2
3
4
5
6
7
8
9
# 1. 构建前端静态文件
cd frontend-shadcn-ui
npm run build
# → 生成 out/ 目录,包含 index.html + _next/ 静态资源 + 所有路由的预渲染 HTML

# 2. 启动后端(同时服务前端 + API)
cd backend
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
# → FastAPI 启动时通过 app.mount('/', SPAStaticFiles(...)) 挂载 out/ 目录

一个端口搞定。访问 http://your-server:8000 拿到前端页面,访问 http://your-server:8000/api/xxx 拿到 API 数据,访问 http://your-server:8000/stock 刷新页面也能正确 fallback 到 index.html

开发模式不受影响

这个方案只影响生产部署,开发体验完全不变:

1
2
3
# 开发时:两个端口,各自热更新
cd frontend-shadcn-ui && npm run dev # → :3000,Next.js dev server + HMR
cd backend && uvicorn app.main:app --reload # → :8000,FastAPI 热重载

Dev 模式下 rewrites 正常工作,前端的 /api/* 请求被代理到后端 :8000。生产环境则直接去掉 rewrites,前后端同源直连。

一次改动,五个问题全消失

问题 原因 单端口后
首页 Loading 卡死 fetchApi 跨域请求失败 同源直连,不存在跨域
移动端菜单打不开 组件状态异常(根因同上) 正常
PC 端子菜单不可见 同上 正常
WebSocket HMR 报错 next start 不该有 dev 代码 生产模式没有 dev server
字体 403 Forbidden 静态文件服务配置问题 FastAPI 直接服务静态文件

这些问题单独看像是五个不同的前端 Bug,但根因是同一个:双端口架构带来的跨域、代理、静态资源服务等一系列连锁问题。

SPA 路由刷新的关键:SPAStaticFiles

普通 StaticFiles 挂载有个问题:如果用户直接访问 /stock(前端路由),FastAPI 会去 out/stock 找文件,找不到就返回 404。但前端路由 /stock 对应的其实是 out/index.html 加载后由 JavaScript 渲染的。

解决办法是继承 StaticFiles 重写 get_response:静态文件找不到时,fallback 到 index.html。同时要排除 /api/docs 路径,避免 API 请求被静态文件处理器拦截。

1
2
3
4
5
6
7
8
9
10
class SPAStaticFiles(StaticFiles):
async def get_response(self, path: str, scope):
if path.startswith("api/") or path.startswith("docs"):
raise StarletteHTTPException(status_code=404)
try:
return await super().get_response(path, scope)
except StarletteHTTPException as exc:
if exc.status_code != 404:
raise
return await super().get_response("index.html", scope)

这段代码参考了港大 HKUDS 实验室的 Vibe-Trading 项目,具体参考了其 agent/api_server.pyserve_main() 函数的静态文件挂载逻辑。他们用同样的模式在 FastAPI 上托管 Vite 构建的 React 前端,单端口跑整个应用。

验证结果

改造完成后用 Playwright 端到端验证:

  • 首页返回真实业务数据(仓库数量、物料种类等),不再卡 Loading
  • Console 无错误出现(之前 WebSocket + 字体错误全部消失)
  • 侧边栏所有导航链接均能正常显示
  • SPA 路由刷新(/stock/login)正确返回 200
  • API 登录正常返回 token

从「五个前端 Bug」到「Console 零错误」,改的只是两个文件各一行配置。架构对了,问题自然消失。


参考文献

  1. Next.js: output: ‘export’ 官方文档
  2. FastAPI: StaticFiles 官方文档
  3. HKUDS/Vibe-Trading — 香港大学数据科学实验室 AI 交易 Agent,单端口部署参考实现
  4. tiangolo/full-stack-fastapi-template — FastAPI 官方全栈模板,包含 SPA 前端集成参考
  5. Next.js: Exporting your App — Limitations — 静态导出模式下 rewrites 等服务器端功能不支持的官方说明

一个端口搞定前后端:Next.js 静态导出 + FastAPI 单服务部署
https://normdist.com/2026/07/21/ND-20260721-002-nextjs-fastapi-single-port-deployment/
作者
小瑞
发布于
2026年7月21日
许可协议