Next.js 静态导出:那些你不知道的坑(附完美避坑方案)
Next.js 静态导出:那些你不知道的坑(附完美避坑方案)
为什么我明明用 Next.js 写得好好的,一开
output: 'export'就报错?
为什么我的 API 代理在本地跑得飞起,部署到静态托管就 404?
为什么我的环境变量到了线上就变不回原来的值?
如果你也有这些困惑,恭喜你——你正在经历从“Next.js 新手”到“Next.js 老油条”的必经之路。
我最近在做 FastAPI + Next.js 前后端分离项目,为了极致节约资源、随处部署,铁了心要用纯静态导出(output: 'export')。本以为 Next.js 这么成熟,静态导出就是 next build 一下,结果一路踩坑踩到怀疑人生。
今天就把我用真金白银的时间换来的经验整理出来,送给所有想用 Next.js 做纯静态网站的你。
💥 坑一:动态路由,静态导出的死敌
我的项目里有一个文章详情页,路由长这样:
app/article/[id]/page.tsx
很常见的动态路由对吧?然后我在 next.config.js 里加了:
output: 'export'
接着 npm run build —— 报错:
Error: Page "/article/[id]" is missing "generateStaticParams()"
so it cannot be used with "output: export" config.
原因:静态导出必须在构建时就生成所有页面的 HTML。你给了一个动态参数 [id],Next.js 根本不知道有哪些 id,自然没法提前生成页面。
✅ 解法 1:generateStaticParams(适合文章数可控、构建时可访问 API)
在页面文件里加上:
export async function generateStaticParams() {
const res = await fetch('https://api.example.com/articles')
const articles = await res.json()
return articles.map(article => ({ id: String(article.id) }))
}
这样构建时会自动生成 /article/1.html、/article/2.html……完美。
但是——如果文章成千上万,或者你压根不想在构建时依赖后端 API,这就尴尬了。
✅ 解法 2:改用查询参数,彻底消灭动态路由(我的最终选择)
把路由改成:
app/article/page.tsx
然后用 ?id=xxx 传参:
'use client';
import { useSearchParams } from 'next/navigation';
const searchParams = useSearchParams();
const id = searchParams.get('id');
fetch(`/api/article/${id}`); // 或者完整后端地址
优点:零构建时依赖,纯静态文件,随便扔哪里都能跑。
代价:URL 从 /article/123 变成 /article?id=123 —— 如果你不在意 SEO,这根本不算代价。
我选这个,因为这个项目是内部工具,Google 爬虫进不来。
如果你要做公开内容站,建议评估一下 SEO 影响——但别被吓倒,Google 已经能抓 CSR 页面了。
💥 坑二:rewrites 在静态导出下就是个摆设
为了让前端代码优雅地写 /api/v1/xxx,我在 next.config.js 里配了代理:
async rewrites() {
return [
{
source: '/api/v1/:path*',
destination: 'http://localhost:8000/api/v1/:path*',
}
]
}
本地 npm run dev 爽歪歪,一打包部署到 Nginx,所有 API 请求全 404。
为什么?
因为 rewrites 是 Next.js 服务器的功能。当你 output: 'export' 后,产出的只是一堆 .html 文件,压根没有服务器在跑,自然没有任何代理规则生效。
✅ 解法 A:放弃幻想,前端直接写完整 URL
const API_BASE = 'https://api.example.com';
fetch(`${API_BASE}/api/v1/articles`);
配合运行时配置文件(见坑三),一套代码跑遍开发、测试、生产。
✅ 解法 B:如果你有 Nginx 控制权,在 Nginx 层代理
location /api/v1/ {
proxy_pass http://your-fastapi:8000/api/v1/;
}
前端代码依然可以写 /api/v1/xxx,但部署环境必须配 Nginx,不能直接扔对象存储。
我选了解法 A,因为我要的是“能扔到任何静态托管平台”。
💥 坑三:NEXT_PUBLIC_* 环境变量,构建时硬编码
以前我都是这样用:
NEXT_PUBLIC_API_URL=https://api.example.com npm run build
代码里:
fetch(process.env.NEXT_PUBLIC_API_URL + '/users')
本地没问题,直到我需要同一份构建包部署到多个环境(开发、测试、生产)。
不行,因为 NEXT_PUBLIC_* 在 next build 时就写死在 JS 文件里了,改不了。
✅ 终极方案:运行时配置文件
在 public 目录下放一个 config.js:
window.__RUNTIME_CONFIG__ = {
API_BASE_URL: 'http://localhost:8000' // 默认值
}
在 app/layout.tsx 中引入:
<Script src="/config.js" strategy="beforeInteractive" />
代码里读取:
const baseUrl = typeof window !== 'undefined'
? window.__RUNTIME_CONFIG__?.API_BASE_URL
: '';
部署时,直接修改服务器上的 config.js 文件,一行命令都不用重新构建。
支持任意静态托管(Nginx、S3、GitHub Pages),这才是真正的“一处编译,到处运行”。
🧠 总结:静态导出,到底适合谁?
| 你的项目特点 | 是否适合纯静态导出 | 推荐方案 |
|---|---|---|
| 内容站,SEO 要求高,文章数少 | ✅ 非常适合 | generateStaticParams |
| 内容站,文章成千上万 | ⚠️ 谨慎评估 | 考虑 ISR 或 SSR |
| 内部工具、后台管理 | ✅ 极适合 | 查询参数 + 运行时配置 |
| 需要 Next.js API Routes | ❌ 不适合 | 必须跑 Node.js 服务器 |
静态导出最纯粹的价值:零运行时服务器、零构建时依赖、极低成本、极致弹性。
为了实现这个价值,你需要放弃:
- 动态路由(除非预生成)
- 内置代理 / 重写
- 构建时注入的环境变量
这些放弃,换来的是一份可以跑在任何地方的纯前端代码。
🎁 彩蛋:查询参数也能拥有“干净” URL
如果你既想用查询参数,又想让用户看到 /article/123 这样的漂亮路径,可以在 Nginx / Vercel / Netlify 层做重写。
Nginx 示例:
location /article/ {
rewrite ^/article/(\d+)$ /article?id=$1 last;
}
用户访问 /article/123 → Nginx 内部重写成 /article?id=123 → 你的静态页面依然能拿到参数。
代码完全不用改,完美。
✍️ 最后
Next.js 是个强大的框架,但它的静态导出模式有自己的一套游戏规则。别试图把服务端的功能硬塞进静态导出里——当你选择静态导出,就请彻底拥抱“纯静态”哲学。
希望这篇文章能让你少踩几个坑。如果你也遇到过其他奇怪的静态导出问题,欢迎在评论区留言分享 👇
本文基于真实项目经验,技术栈为 Next.js + FastAPI。
本文来自博客园,作者:Athenavi,转载请注明原文链接:https://www.cnblogs.com/Athenavi/p/19613060

为什么我明明用 Next.js 写得好好的,一开 output: 'export' 就报错?
浙公网安备 33010602011771号