迪士尼在逃国王 發表於 2026-2-13 17:43:00

Next.js 静态导出:那些你不知道的坑(附完美避坑方案)

<h1 id="nextjs-静态导出那些你不知道的坑附完美避坑方案">Next.js 静态导出:那些你不知道的坑(附完美避坑方案)</h1>
<blockquote>
<p>为什么我明明用 Next.js 写得好好的,一开 <code>output: 'export'</code> 就报错?<br>
为什么我的 API 代理在本地跑得飞起,部署到静态托管就 404?<br>
为什么我的环境变量到了线上就变不回原来的值?</p>
</blockquote>
<p>如果你也有这些困惑,恭喜你——<strong>你正在经历从“Next.js 新手”到“Next.js 老油条”的必经之路</strong>。</p>
<p>我最近在做 <strong>FastAPI + Next.js 前后端分离项目</strong>,为了极致节约资源、随处部署,铁了心要用纯静态导出(<code>output: 'export'</code>)。本以为 Next.js 这么成熟,静态导出就是 <code>next build</code> 一下,结果一路踩坑踩到怀疑人生。</p>
<p>今天就把<strong>我用真金白银的时间换来的经验</strong>整理出来,送给所有想用 Next.js 做纯静态网站的你。</p>
<hr>
<h2 id="-坑一动态路由静态导出的死敌">💥 坑一:动态路由,静态导出的死敌</h2>
<p>我的项目里有一个文章详情页,路由长这样:</p>
<pre><code>app/article//page.tsx
</code></pre>
<p>很常见的动态路由对吧?然后我在 <code>next.config.js</code> 里加了:</p>
<pre><code class="language-js">output: 'export'
</code></pre>
<p>接着 <code>npm run build</code> —— <strong>报错</strong>:</p>
<pre><code>Error: Page "/article/" is missing "generateStaticParams()"
so it cannot be used with "output: export" config.
</code></pre>
<p><strong>原因</strong>:静态导出必须在<strong>构建时</strong>就生成所有页面的 HTML。你给了一个动态参数 <code></code>,Next.js 根本不知道有哪些 id,自然没法提前生成页面。</p>
<hr>
<h3 id="-解法-1generatestaticparams适合文章数可控构建时可访问-api">✅ 解法 1:<code>generateStaticParams</code>(适合文章数可控、构建时可访问 API)</h3>
<p>在页面文件里加上:</p>
<pre><code class="language-tsx">export async function generateStaticParams() {
const res = await fetch('https://api.example.com/articles')
const articles = await res.json()
return articles.map(article =&gt; ({ id: String(article.id) }))
}
</code></pre>
<p>这样构建时会自动生成 <code>/article/1.html</code>、<code>/article/2.html</code>……完美。</p>
<p><strong>但是</strong>——如果文章成千上万,或者你压根不想在构建时依赖后端 API,这就尴尬了。</p>
<hr>
<h3 id="-解法-2改用查询参数彻底消灭动态路由我的最终选择">✅ 解法 2:改用查询参数,彻底消灭动态路由(我的最终选择)</h3>
<p>把路由改成:</p>
<pre><code>app/article/page.tsx
</code></pre>
<p>然后用 <code>?id=xxx</code> 传参:</p>
<pre><code class="language-tsx">'use client';
import { useSearchParams } from 'next/navigation';

const searchParams = useSearchParams();
const id = searchParams.get('id');
fetch(`/api/article/${id}`); // 或者完整后端地址
</code></pre>
<p><strong>优点</strong>:零构建时依赖,纯静态文件,随便扔哪里都能跑。<br>
<strong>代价</strong>:URL 从 <code>/article/123</code> 变成 <code>/article?id=123</code> —— 如果你不在意 SEO,这根本不算代价。</p>
<blockquote>
<p>我选这个,因为这个项目是内部工具,Google 爬虫进不来。<br>
如果你要做公开内容站,建议评估一下 SEO 影响——<strong>但别被吓倒,Google 已经能抓 CSR 页面了</strong>。</p>
</blockquote>
<hr>
<h2 id="-坑二rewrites-在静态导出下就是个摆设">💥 坑二:<code>rewrites</code> 在静态导出下就是个摆设</h2>
<p>为了让前端代码优雅地写 <code>/api/v1/xxx</code>,我在 <code>next.config.js</code> 里配了代理:</p>
<pre><code class="language-js">async rewrites() {
return [
    {
      source: '/api/v1/:path*',
      destination: 'http://localhost:8000/api/v1/:path*',
    }
]
}
</code></pre>
<p>本地 <code>npm run dev</code> 爽歪歪,<strong>一打包部署到 Nginx,所有 API 请求全 404</strong>。</p>
<p><strong>为什么?</strong><br>
因为 <code>rewrites</code> 是 <strong>Next.js 服务器的功能</strong>。当你 <code>output: 'export'</code> 后,产出的只是一堆 <code>.html</code> 文件,压根没有服务器在跑,自然没有任何代理规则生效。</p>
<hr>
<h3 id="-解法-a放弃幻想前端直接写完整-url">✅ 解法 A:放弃幻想,前端直接写完整 URL</h3>
<pre><code class="language-ts">const API_BASE = 'https://api.example.com';
fetch(`${API_BASE}/api/v1/articles`);
</code></pre>
<p>配合<strong>运行时配置文件</strong>(见坑三),一套代码跑遍开发、测试、生产。</p>
<hr>
<h3 id="-解法-b如果你有-nginx-控制权在-nginx-层代理">✅ 解法 B:如果你有 Nginx 控制权,在 Nginx 层代理</h3>
<pre><code class="language-nginx">location /api/v1/ {
    proxy_pass http://your-fastapi:8000/api/v1/;
}
</code></pre>
<p>前端代码依然可以写 <code>/api/v1/xxx</code>,但<strong>部署环境必须配 Nginx</strong>,不能直接扔对象存储。</p>
<hr>
<p><strong>我选了解法 A</strong>,因为我要的是“<strong>能扔到任何静态托管平台</strong>”。</p>
<hr>
<h2 id="-坑三next_public_-环境变量构建时硬编码">💥 坑三:<code>NEXT_PUBLIC_*</code> 环境变量,构建时硬编码</h2>
<p>以前我都是这样用:</p>
<pre><code class="language-bash">NEXT_PUBLIC_API_URL=https://api.example.com npm run build
</code></pre>
<p>代码里:</p>
<pre><code class="language-ts">fetch(process.env.NEXT_PUBLIC_API_URL + '/users')
</code></pre>
<p>本地没问题,直到我需要<strong>同一份构建包部署到多个环境</strong>(开发、测试、生产)。<br>
<strong>不行</strong>,因为 <code>NEXT_PUBLIC_*</code> 在 <code>next build</code> 时就<strong>写死在 JS 文件里了</strong>,改不了。</p>
<hr>
<h3 id="-终极方案运行时配置文件">✅ 终极方案:运行时配置文件</h3>
<p>在 <code>public</code> 目录下放一个 <code>config.js</code>:</p>
<pre><code class="language-js">window.__RUNTIME_CONFIG__ = {
API_BASE_URL: 'http://localhost:8000'// 默认值
}
</code></pre>
<p>在 <code>app/layout.tsx</code> 中引入:</p>
<pre><code class="language-tsx">&lt;Script src="/config.js" strategy="beforeInteractive" /&gt;
</code></pre>
<p>代码里读取:</p>
<pre><code class="language-ts">const baseUrl = typeof window !== 'undefined'
? window.__RUNTIME_CONFIG__?.API_BASE_URL
: '';
</code></pre>
<p><strong>部署时,直接修改服务器上的 <code>config.js</code> 文件,一行命令都不用重新构建。</strong><br>
支持任意静态托管(Nginx、S3、GitHub Pages),这才是真正的<strong>“一处编译,到处运行”</strong>。</p>
<hr>
<h2 id="-总结静态导出到底适合谁">🧠 总结:静态导出,到底适合谁?</h2>
<table>
<thead>
<tr>
<th>你的项目特点</th>
<th>是否适合纯静态导出</th>
<th>推荐方案</th>
</tr>
</thead>
<tbody>
<tr>
<td>内容站,SEO 要求高,文章数少</td>
<td>✅ 非常适合</td>
<td><code>generateStaticParams</code></td>
</tr>
<tr>
<td>内容站,文章成千上万</td>
<td>⚠️ 谨慎评估</td>
<td>考虑 ISR 或 SSR</td>
</tr>
<tr>
<td>内部工具、后台管理</td>
<td>✅ 极适合</td>
<td>查询参数 + 运行时配置</td>
</tr>
<tr>
<td>需要 Next.js API Routes</td>
<td>❌ 不适合</td>
<td>必须跑 Node.js 服务器</td>
</tr>
</tbody>
</table>
<hr>
<p><strong>静态导出最纯粹的价值</strong>:<strong>零运行时服务器、零构建时依赖、极低成本、极致弹性</strong>。</p>
<p>为了实现这个价值,你需要放弃:</p>
<ul>
<li>动态路由(除非预生成)</li>
<li>内置代理 / 重写</li>
<li>构建时注入的环境变量</li>
</ul>
<p><strong>这些放弃,换来的是一份可以跑在任何地方的纯前端代码。</strong></p>
<hr>
<h2 id="-彩蛋查询参数也能拥有干净-url">🎁 彩蛋:查询参数也能拥有“干净” URL</h2>
<p>如果你既想用查询参数,又想让用户看到 <code>/article/123</code> 这样的漂亮路径,<strong>可以在 Nginx / Vercel / Netlify 层做重写</strong>。</p>
<p>Nginx 示例:</p>
<pre><code class="language-nginx">location /article/ {
    rewrite ^/article/(\d+)$ /article?id=$1 last;
}
</code></pre>
<p>用户访问 <code>/article/123</code> → Nginx 内部重写成 <code>/article?id=123</code> → 你的静态页面依然能拿到参数。<br>
<strong>代码完全不用改,完美。</strong></p>
<hr>
<h2 id="️-最后">✍️ 最后</h2>
<p>Next.js 是个强大的框架,但它的静态导出模式有自己的一套游戏规则。别试图把服务端的功能硬塞进静态导出里——<strong>当你选择静态导出,就请彻底拥抱“纯静态”哲学</strong>。</p>
<p>希望这篇文章能让你少踩几个坑。如果你也遇到过其他奇怪的静态导出问题,<strong>欢迎在评论区留言分享</strong> 👇</p>
<hr>
<p><em>本文基于真实项目经验,技术栈为 Next.js + FastAPI。</em></p>


</div>
<div id="MySignature" role="contentinfo">
    <p>本文来自博客园,作者:Athenavi,转载请注明原文链接:https://www.cnblogs.com/Athenavi/p/19613060</p><br><br>
来源:https://www.cnblogs.com/Athenavi/p/19613060
頁: [1]
查看完整版本: Next.js 静态导出:那些你不知道的坑(附完美避坑方案)