跳到正文
花城
返回文章列表

Next.js 静态导出的五个坑

从 Server Actions 到图片优化,把 output export 之后会突然失效的东西一次性列清楚,顺便给出替代方案。

全文 915 字作者:花城浏览与点赞统计加载中

先说结论#

output: "export" 不是「构建得更彻底一点」,而是放弃了一整类能力。 它会把你从「有一个 Node 进程可以跑代码」的假设里拽出来,回到「我只有一堆文件」。

一开始我以为这只是打包方式变了,直到 CI 报了一堆错才明白,是运行时的假设变了。

坑一:Server Actions 直接不可用#

写表单的时候顺手用了 Server Action:

async function subscribe(formData: FormData) {
  "use server";
  await db.insert(formData.get("email"));
}

静态导出下这段代码连编译都过不去,因为 Server Action 需要一个能接收 POST 的运行时。

替代方案是回到最朴素的做法——把表单指向一个第三方的无服务器函数:

<form action="https://api.example.com/subscribe" method="post">
  <input name="email" type="email" required />
  <button type="submit">订阅</button>
</form>

Cloudflare Workers 是好朋友

Pages 和 Workers 在同一个账号下,写一个几十行的 Worker 处理表单, 既保住了静态站点的速度,又拿回了「能跑代码」的能力。

坑二:动态路由必须提前列全#

动态路由在静态导出下必须配合 generateStaticParams, 而且要把 dynamicParams 关掉——否则框架不知道还有哪些 slug 需要生成。

export const dynamicParams = false;

export function generateStaticParams() {
  return getAllSlugs().map((slug) => ({ slug }));
}

这样未知的 slug 不会在运行时被渲染,而是直接由静态托管返回 404。

坑三:图片优化器需要服务端#

默认情况下 next/image 会请求 /_next/image,那是一个需要 Node 进程的接口。 静态导出时有两种选择:

方案写法适用场景
关闭优化images: { unoptimized: true }图片不多,直接用原图
自定义 loaderimages: { loader: "custom", loaderFile: "./loader.ts" }交给 Cloudflare Images 之类的服务

我选的是第一种。博客配图本来就少,而且都是我自己压过的, 再套一层优化收益不大,反而多一个失败点。

坑四:getStaticProps 是 Pages Router 的东西#

搜资料的时候经常看到这样的写法:

export async function getStaticProps() { … }

这是 Pages Router 的 API,在 App Router 里完全不生效——而且不会报错, 它只是被当成一个普通的导出忽略掉,然后你会发现页面数据永远是空的。

App Router 的做法是直接在组件里 await,因为这个组件本身就运行在构建期:

export default async function Page() {
  const posts = getAllPostMeta(); // 构建时执行,结果写进 HTML
  return <PostList posts={posts} />;
}

别在客户端组件里读文件

读 node:fs 只能在服务端组件里做。一旦文件顶部写了 "use client", 再 import fs 就会把 Node 内置模块打进浏览器 bundle,构建直接失败。

坑五:环境变量与 Windows 脚本#

next build 时 NODE_ENV 是 production,所以任何「开发环境下显示草稿」的逻辑 在上线时都会自动关掉——这通常是你想要的,但要记得它确实发生了。

另一个更琐碎的问题是跨平台脚本。Windows 的 cmd 里写:

"dev": "set TINA_PUBLIC_IS_LOCAL=true && tinacms dev -c \"next dev\""

看起来没问题,实际上 set 会把 true 后面那个空格也当成值的一部分, 最后变量变成了 "true "。用 cross-env 可以绕开这类坑:

"dev": "cross-env TINA_PUBLIC_IS_LOCAL=true tinacms dev -c \"next dev\""

那到底值不值得#

值得,但要看你的场景。

静态站点用「失去动态能力」换来了极低的运维成本和极快的响应速度。 对个人博客来说,这个交换非常划算:内容一天最多更新几次, 但页面一天要被读很多次,而 CDN 恰好擅长后者。

真正需要重新考虑的临界点,大概是你开始想要「每个用户看到不同的内容」的时候。 到那一天,再把 Worker 或者 D1 加进来也不迟。

评论0

0/1000

还没有评论,来写第一条吧。

访客的评论先存在你自己的浏览器里(纯静态站没有收件箱), 只有站长发布的留言与回复是写进仓库、所有人都能看到的;点赞也只记在本机。

想要「所有访客都看得见、站长能回复」的评论区,两条路选一条:部署仓库里自带的互动服务 (workers/blog-api,见 README),或者配置 Giscus。 现在既没配也没部署,所以上面的评论只存在各自的浏览器里。

相关文章