一个端口搞定前后端: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 | |
这段代码的逻辑完全正确:API 请求和静态文件请求分流,前端路由 404 时 fallback 到 index.html(SPA 刷新支持)。
但它从来没生效过,因为两个问题:
- 路径不对:
FRONTEND_DIST_DIR指向frontend/dist,但 Next.js 默认输出到.next/目录,根本没有dist/ - Next.js 不是纯静态应用:默认 SSR 模式下,
.next/目录里是运行时文件,不是纯静态 HTML/JS/CSS,FastAPI 无法直接托管
解决方案:两行改动
改动一:Next.js 开启静态导出
next.config.ts 改两行:
1 | |
output: 'export' 是 Next.js 的静态导出模式。执行 npm run build 后,所有页面会被预渲染成静态 HTML/JS/CSS 文件,输出到 out/ 目录。所有路由页面全部变成纯静态文件,不再需要 Node.js 运行时。
注意:静态导出模式下 rewrites、redirects、headers 等服务器端功能自动失效,这正是我们想要的——生产环境不需要代理层,因为前后端已经跑在同一个端口上了。开发模式下 rewrites 仍然在 next dev 中正常工作。
改动二:修正 FastAPI 静态文件路径
项目实际的前端目录是 frontend-shadcn-ui(改前路径指向的 frontend/dist 既不存在、命名也与实际不符)。
1 | |
就这样。构建前端 → 重启后端 → 一个端口同时服务 API 和前端。
构建和部署流程
1 | |
一个端口搞定。访问 http://your-server:8000 拿到前端页面,访问 http://your-server:8000/api/xxx 拿到 API 数据,访问 http://your-server:8000/stock 刷新页面也能正确 fallback 到 index.html。
开发模式不受影响
这个方案只影响生产部署,开发体验完全不变:
1 | |
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 | |
这段代码参考了港大 HKUDS 实验室的 Vibe-Trading 项目,具体参考了其 agent/api_server.py 中 serve_main() 函数的静态文件挂载逻辑。他们用同样的模式在 FastAPI 上托管 Vite 构建的 React 前端,单端口跑整个应用。
验证结果
改造完成后用 Playwright 端到端验证:
- 首页返回真实业务数据(仓库数量、物料种类等),不再卡 Loading
- Console 无错误出现(之前 WebSocket + 字体错误全部消失)
- 侧边栏所有导航链接均能正常显示
- SPA 路由刷新(
/stock、/login)正确返回 200 - API 登录正常返回 token
从「五个前端 Bug」到「Console 零错误」,改的只是两个文件各一行配置。架构对了,问题自然消失。
参考文献
- Next.js: output: ‘export’ 官方文档
- FastAPI: StaticFiles 官方文档
- HKUDS/Vibe-Trading — 香港大学数据科学实验室 AI 交易 Agent,单端口部署参考实现
- tiangolo/full-stack-fastapi-template — FastAPI 官方全栈模板,包含 SPA 前端集成参考
- Next.js: Exporting your App — Limitations — 静态导出模式下 rewrites 等服务器端功能不支持的官方说明