返回博客

Next.js 静态导出的动态路由必须拆服务端壳

项目配置了 output: "export" 做纯静态导出,准备部署到 CDN。新加一个带动态段的路由 app/xxx/[id]/page.tsx,因为页面要用 useStateuseRouter,顺手在文件顶部写了 "use client",然后 next dev 直接弹出红色 overlay,next build 也挂了。

现象

Page "/xxx/[id]/page" is missing exported function "generateStaticParams()",
which is required with "output: export" config.

错误在 dev 运行时 overlay 和 next build 两处都能复现,tsc 类型检查阶段却完全静默。

根因

静态导出模式下,Next.js 在构建期必须枚举所有动态路径,才能生成对应的 [id].html 文件。枚举的唯一入口是页面文件导出的 generateStaticParams() 函数——这是一个 Server-only 的导出。

问题在于:"use client" 标记会把整个模块提交给客户端 bundle,服务端专属的导出(generateStaticParams)不允许出现在 Client Component 模块里。你即便硬写进去,Next.js 也会在运行时忽略或报错,框架层面就封死了这条路。

坑:use client 页面无法导出 generateStaticParams

只要 page.tsx 顶部有 "use client",在该文件内写 export function generateStaticParams() 对 Next.js 无效——构建器要的是服务端模块的导出,Client Component 模块里的同名函数会被忽略,构建因"缺少 generateStaticParams"而失败。

正解

把路由拆成两层:服务端壳页面 + 客户端实体组件

第一层:app/xxx/[id]/page.tsx(Server Component,无 "use client"

// app/xxx/[id]/page.tsx  —— 服务端壳,只负责两件事:枚举路径 + 渲染客户端子组件
import { ItemDetail } from "../_components/item-detail";
import { items } from "../_data/items";
 
export function generateStaticParams() {
  return items.map((item) => ({ id: item.id }));
}
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params; // Next 15/16 中 params 是 Promise,需要 await
  return <ItemDetail id={id} />;
}

第二层:_components/item-detail.tsx(Client Component)

// _components/item-detail.tsx  —— 所有交互状态都在这里
"use client";
 
export function ItemDetail({ id }: { id: string }) {
  // useState、useRouter、useEffect 随意使用
}

关键变化:原来页面里的 useParams() 改为接收 id prop,因为 id 在服务端壳里已经 await params 拿到了。

正解:服务端壳导出路径枚举,客户端子组件持有交互状态

Server Component 页面负责 generateStaticParams(枚举路径)和 await params(解析当前 id),然后把 id 作为 prop 传给 "use client" 子组件。静态导出照常生成每个 id 的 HTML,客户端 hydration 后交互正常。

补充说明

  • Next.js 15/16 的 Server Component 中,params 是一个 Promise,必须 await 再解构,不能像旧版直接解构。
  • generateStaticParams 返回的数组内容可以来自静态数据、JSON 文件或构建时脚本——静态导出只要求路径集合在构建期已知,不强制走数据库。
  • 静态导出的其他限制:不支持 getServerSideProps、不支持运行时动态路由、不支持 middleware 重写路径。

一句话外卖

output: "export" 下,动态路由页面必须是 Server Component 壳来承载 generateStaticParams,交互逻辑下沉到接收 prop 的 Client Component 子组件,不能把两者塞进同一个 "use client" 文件。