发布日期

第 06 讲|Manifest 体系:App 是怎么"打开"的

Manifest 体系解析:AppPathsManifest / RoutesManifest 如何驱动请求分发

阶段二:App Router 核心机制 · 第 6 / 40 讲 难度:⭐⭐⭐⭐ · 预计耗时:3 小时 配套 fixture:fixtures/lecture-06/

学习目标

学完本讲,你应当能:

  • 默写 .next/ 下至少 10 个 manifest 的名字、产生者、消费者。
  • 一眼看出某个生产事故是「manifest 写漏」「manifest 路径错」还是「manifest 读路径错」。
  • 在 fixture 中亲手 cat .next/server/app-paths-manifest.json 看到自己写的 page 出现在里面。
  • 理解 client-reference-manifestserver-reference-manifest —— 这两张表是 RSC 整套机制的"导线图"。

1. 为什么要有 Manifest?

App Router 的运行时(base-server / app-render)在请求进来时需要回答四类问题:

  1. 「这个 URL 对应哪个文件?」app-paths-manifest.json
  2. 「这个 page 渲染需要加载哪些 chunk?」build-manifest.json / app-build-manifest.json
  3. 「这个 client 组件在 RSC payload 中怎么引用?」client-reference-manifest(核心)
  4. 「这个 Server Action ID 是哪个函数?」server-reference-manifest

manifest 就是为了让运行时O(1) 查表而不是 O(n) 扫描文件系统/AST。每张 manifest 都是 webpack/turbopack 在编译期产生 → JSON 文件落盘 → 运行时 require 进来。

你可以把 manifest 体系理解成 「编译期把所有运行时不能拖延的事提前做完」——这是所有 production-grade 框架都会有的设计。

2. 全景图

按"被谁产生 / 被谁消费"分类,这是 Next.js 当前的 manifest 全景:

┌───────────────────────────────────────────────────────────────────┐
BUILD│                                                                   │
│  webpack/turbopack plugins:│  ├─ PagesManifestPlugin     → app-paths-manifest.json│  │                          → pages-manifest.json│  ├─ BuildManifestPlugin     → build-manifest.json│  │                          → app-build-manifest.json│  ├─ FlightManifestPlugin*_client-reference-manifest.js│  ├─ FlightClientEntryPlugin → server-reference-manifest.json/.js│  ├─ MiddlewarePlugin        → middleware-manifest.json│  ├─ NextFontManifestPlugin  → next-font-manifest.json│  ├─ ReactLoadablePlugin     → react-loadable-manifest.json│  └─ NextTracePlugin*.nft.json (单文件 trace)│                                                                   │
│  build/index.ts 主流程:                                            │
│  ├─ generateRoutesManifest  → routes-manifest.json│  ├─ collect prerender data  → prerender-manifest.json│  ├─ collectBuildTraces      → required-server-files.json│  ├─ ...└──────────────────────┬────────────────────────────────────────────┘
                       ▼ 落盘到 .next/
┌───────────────────────────────────────────────────────────────────┐
SERVE│                                                                   │
│  base-server.ts / next-server.ts:│  ├─ loadManifest("app-paths-manifest.json")│  ├─ loadManifest("routes-manifest.json")│  ├─ loadManifest("prerender-manifest.json")│  ├─ loadManifest("middleware-manifest.json")│  └─ ...│                                                                   │
│  route-modules/app-page/module.ts:│  ├─ require(client-reference-manifest)│  ├─ require(server-reference-manifest)│  └─ ...└───────────────────────────────────────────────────────────────────┘

3. Manifest 命名宪法

packages/next/src/shared/lib/constants.ts 里把所有 manifest 的文件名都集中起来,建议你把这一段当作 必背单词表

138:packages/next/src/shared/lib/constants.ts
export const PAGES_MANIFEST = 'pages-manifest.json'
export const APP_PATHS_MANIFEST = 'app-paths-manifest.json'
export const APP_PATH_ROUTES_MANIFEST = 'app-path-routes-manifest.json'
export const BUILD_MANIFEST = 'build-manifest.json'
export const FUNCTIONS_CONFIG_MANIFEST = 'functions-config-manifest.json'
export const SUBRESOURCE_INTEGRITY_MANIFEST = 'subresource-integrity-manifest'
export const NEXT_FONT_MANIFEST = 'next-font-manifest'
export const EXPORT_MARKER = 'export-marker.json'
export const EXPORT_DETAIL = 'export-detail.json'
export const PRERENDER_MANIFEST = 'prerender-manifest.json'
export const PREFETCH_HINTS = 'prefetch-hints.json'
export const ROUTES_MANIFEST = 'routes-manifest.json'
export const IMAGES_MANIFEST = 'images-manifest.json'
export const SERVER_FILES_MANIFEST = 'required-server-files'
export const DEV_CLIENT_PAGES_MANIFEST = '_devPagesManifest.json'
export const MIDDLEWARE_MANIFEST = 'middleware-manifest.json'
export const TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST =
  '_clientMiddlewareManifest.js'
export const TURBOPACK_CLIENT_BUILD_MANIFEST = 'client-build-manifest.json'
export const DEV_CLIENT_MIDDLEWARE_MANIFEST = '_devMiddlewareManifest.json'
export const REACT_LOADABLE_MANIFEST = 'react-loadable-manifest.json'
export const SERVER_DIRECTORY = 'server'
export const CONFIG_FILES = [
  'next.config.js',
  'next.config.mjs',
  'next.config.ts',
  // process.features can be undefined on Edge runtime
  // TODO: Remove `as any` once we bump @types/node to v22.10.0+
  ...((process?.features as any)?.typescript ? ['next.config.mts'] : []),
]
export const BUILD_ID_FILE = 'BUILD_ID'
export const BLOCKED_PAGES = ['/_document', '/_app', '/_error']
export const CLIENT_PUBLIC_FILES_PATH = 'public'
export const CLIENT_STATIC_FILES_PATH = 'static'
export const STRING_LITERAL_DROP_BUNDLE = '__NEXT_DROP_CLIENT_FILE__'
export const NEXT_BUILTIN_DOCUMENT = '__NEXT_BUILTIN_DOCUMENT__'
export const BARREL_OPTIMIZATION_PREFIX = '__barrel_optimize__'

// server/[entry]/page_client-reference-manifest.js
export const CLIENT_REFERENCE_MANIFEST = 'client-reference-manifest'
// server/server-reference-manifest
export const SERVER_REFERENCE_MANIFEST = 'server-reference-manifest'
// server/middleware-build-manifest.js
export const MIDDLEWARE_BUILD_MANIFEST = 'middleware-build-manifest'
// server/middleware-react-loadable-manifest.js
export const MIDDLEWARE_REACT_LOADABLE_MANIFEST =
  'middleware-react-loadable-manifest'
// server/interception-route-rewrite-manifest.js
export const INTERCEPTION_ROUTE_REWRITE_MANIFEST =
  'interception-route-rewrite-manifest'
// server/dynamic-css-manifest.js
export const DYNAMIC_CSS_MANIFEST = 'dynamic-css-manifest'

注意几个隐藏细节:

  • 下划线开头 的(_devPagesManifest.json / _devMiddlewareManifest.json / _clientMiddlewareManifest.js)是 dev 模式专用。
  • .js 结尾 的(*_client-reference-manifest.jsserver-reference-manifest.js)是因为它们需要被 webpack runtime require,所以打成 JS 而非 JSON。
  • .nft.json 不在这个常量表里——它由 next-trace-entrypoints-plugin 产生,每个 entry 一份。

4. 关键 Manifest 逐个拆

4.1 app-paths-manifest.json —— URL → 文件

最朴素也最常用。结构:

{
  "/page": "app/page.js",
  "/(marketing)/about/page": "app/(marketing)/about/page.js",
  "/products/[id]/page": "app/(shop)/products/[id]/page.js",
  "/products/@modal/(.)[id]/page": "app/(shop)/products/@modal/(.)[id]/page.js"
}

注意三件事

  1. key 用 normalized internal path(带 /page / /route 后缀、保留 route group 与 parallel slot)。这是 framework internal pathname,不是 URL。要拿到 URL 还得过 normalizeAppPath
  2. value 是相对 .next/server/ 的 .js 路径——这是 route-modules/app-page 实际 require() 的目标。
  3. 同时存在 node 和 edge runtime 时,会有 app-paths-manifest.json 和对应 edge 版本

产生者:pages-manifest-plugin.ts,文件里同时累积 pages 与 app,最后吐两份:

90:packages/next/src/build/webpack/plugins/pages-manifest-plugin.ts
  async createAssets(compilation: any) {
    const entrypoints = compilation.entrypoints
    const pages: PagesManifest = {}
    const appPaths: PagesManifest = {}

    for (const entrypoint of entrypoints.values()) {
      const pagePath = getRouteFromEntrypoint(
        entrypoint.name,
        this.appDirEnabled
      )

      if (!pagePath) {
        continue
      }

      const files = entrypoint
        .getFiles()
        .filter(
          (file: string) =>
            !file.includes('webpack-runtime') &&
            !file.includes('webpack-api-runtime') &&
            file.endsWith('.js')
        )

      // Skip entries which are empty
      if (!files.length) {
        continue
      }
      // Write filename, replace any backslashes in path (on windows) with forwardslashes for cross-platform consistency.
      let file = files[files.length - 1]

      if (!this.dev) {
        if (!this.isEdgeRuntime) {
          file = file.slice(3)
        }

消费者:base-server.tsgetAppPathsManifest() 中 require 它,然后注册到 route-matcher-managers。

生产排查提示:用户报 404,先看这个 manifest 里有没有对应 key。如果没有,是 build 漏了(多见于 case:用户改了 next.config.js 的 pageExtensions 但没全量构建)。

4.2 app-path-routes-manifest.json —— URL ↔ Internal Path 双向表

app-paths-manifest.json 的 key 是 internal path(带 group/slot),但客户端只知道真实 URL。Next.js 用 app-path-routes-manifest.json 做反向映射:

{
  "/(marketing)/about/page": "/about",
  "/(shop)/products/[id]/page": "/products/[id]",
  "/products/@modal/(.)[id]/page": "/products/[id]"
}

每个 internal path 对应一个 URL pattern(route group 已剥离)。注意 modal 的 intercepting variant 也指向 /products/[id]——这是 intercepting route 在 URL 上"共享"被拦截路径的体现。

4.3 build-manifest.json —— Pages Router 的"页面 → chunks"

类型定义清楚地写明这是 Pages Router 用的:

17:packages/next/src/server/get-page-files.ts
export type BuildManifest = {
  devFiles: readonly string[]
  polyfillFiles: readonly string[]
  lowPriorityFiles: readonly string[]
  rootMainFiles: readonly string[]
  // this is a separate field for flying shuttle to allow
  // different root main files per entries/build (ideally temporary)
  // until we can stitch the runtime chunks together safely
  rootMainFilesTree: { [appRoute: string]: readonly string[] }
  pages: {
    '/_app': readonly string[]
    [page: string]: readonly string[]
  }
}
  • pages 是 pages router 每个页面所需的 JS/CSS 文件清单(不含 RSC 需求)。
  • rootMainFiles 是被所有页面共用的运行时(webpack runtime、main.js 等)。
  • polyfillFiles 是给老浏览器的兼容代码(来自 packages/next-polyfill-nomodule)。

App Router 有个对应的 app-build-manifest.json,结构类似但 key 用 normalized app path。

4.4 client-reference-manifest(RSC 的"导线图")

这是整个 RSC 体系的核心。每个 App page 都对应一份,路径 .next/server/app/<page>/page_client-reference-manifest.js

类型:

97:packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts
export interface ClientReferenceManifestForRsc {
  clientModules: ManifestNode
  rscModuleMapping: {
    [moduleId: string]: ManifestNode
  }
  edgeRscModuleMapping: {
    [moduleId: string]: ManifestNode
  }
}

export type CssResource = InlinedCssFile | UninlinedCssFile

interface InlinedCssFile {
  path: string
  inlined: true
  content: string
}

interface UninlinedCssFile {
  path: string
  inlined: false
}

一个简化的 clientModules 节点:

clientModules: {
  '/path/to/MyButton.js#default': {
    id: 'webpack-id-123',
    name: 'default',
    chunks: ['static/chunks/MyButton.abc.js'],
    async: false,
  },
}

字段解读

字段含义
key"<absolute-file-path>#<export-name>",唯一标识一个 client component export
idwebpack module ID(数字或字符串,给 react-server-dom-webpack 用)
nameexport 名字('default' / 命名 export)
chunks这个 module 需要的 JS / CSS chunk 文件名列表
async是否含 async module(如 dynamic ESM)

rscModuleMappingedgeRscModuleMapping 用来把 server graph 中的 module id 映射到 client graph 中的 client modules——这是 RSC 实现"在 server 渲染时引用 client 组件占位符"的关键。

消费场景:当 server 渲染流到一个 client component(即 'use client' 模块)时:

  1. RSC 写出占位符($L)+ 引用 id。
  2. 客户端通过 client-reference-manifest 找到 id 对应的 chunk URL。
  3. 客户端 <script> 动态加载该 chunk 后接管这个位置。

第 17 讲会深入 Flight 协议。

生产排查提示:客户端报 Module not found 而服务端正常 → 多半是 client-reference-manifest 写漏(典型场景:自定义 webpack loader 处理了 'use client' 但没向 plugin 注册)。

4.5 server-reference-manifest —— Server Actions 的入口表

类型:

99:packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts
export type ActionManifest = {
  // Assign a unique encryption key during production build.
  encryptionKey: string
  node: Actions
  edge: Actions
}

简化后的结构:

{
  "encryptionKey": "<base64...>",
  "node": {
    "action-hash-1a2b": {
      "workers": {
        "app/products/page": "static/chunks/app/products/page.js"
      },
      "layer": {
        "app/products/page": "action-browser"
      }
    }
  },
  "edge": {
    /* edge runtime 同上 */
  }
}

核心字段

  • encryptionKey:build 时随机生成,用来加密 Server Action 闭包绑定的变量(详见第 12 讲)。这意味着同一份代码不同次 build 的 action 不能互通——刷新部署后旧的 action 调用会失败。
  • node / edge:按 runtime 分桶,每个 action ID(hash)映射到该 action 出现在哪些 entry。

消费场景

  1. 客户端 form 提交,请求体里带 action ID。
  2. 服务端从 server-reference-manifest 找到 action ID 对应的 entry。
  3. 加载对应 worker chunk,调用真正的函数。

生产排查提示:升级 deploy 后老用户报 "Failed to find Server Action" → 一般是 encryptionKey 变了。解决方案:粘贴上一次 build 的 key 到 next.config.jsexperimental.serverActions.encryption_key(详见 docs)。

4.6 middleware-manifest.json

64:packages/next/src/build/webpack/plugins/middleware-plugin.ts
export interface MiddlewareManifest {
  version: 3
  sortedMiddleware: string[]
  middleware: { [page: string]: EdgeFunctionDefinition }
  functions: { [page: string]: EdgeFunctionDefinition }
}
  • middleware:项目根的 middleware.ts(一般只有一项 /)。
  • functions:所有 export const runtime = 'edge' 的 page / route handler。
  • EdgeFunctionDefinition 里有 name / files / matchers / wasm / assets / env——给 edge runtime 跑时用。

这个 manifest 直接决定了 edge runtime 哪些路由被注入到了 edge sandbox。第 21 讲会详细讲。

4.7 prerender-manifest.json —— 静态化清单

build/index.ts 里定义:

386:packages/next/src/build/index.ts
export type PrerenderManifest = {
  version: 4
  routes: { [route: string]: PrerenderManifestRoute }
  dynamicRoutes: { [route: string]: DynamicPrerenderManifestRoute }
  notFoundRoutes: string[]
  preview: __ApiPreviewProps
}
字段含义
routes已经预渲染的静态 URL 列表
dynamicRoutes含有 generateStaticParams 但不固定的,运行时按需 ISR
notFoundRoutes显式 prerender 失败/404 的列表
previewDraft Mode 的预览 props(cookie name 等)

生产排查提示:用户报 "为什么我的 ISR 不刷新?" → 先看这个 manifest,确认该路由在 routes 里、revalidate 不是 false

4.8 routes-manifest.json —— 完整路由配置

443:packages/next/src/build/index.ts
export type RoutesManifest = {
  version: number
  pages404: boolean
  appType: 'app' | 'pages' | 'hybrid'
  basePath: string
  redirects: Array<ManifestRedirectRoute>
  rewrites: {
    beforeFiles: Array<ManifestRewriteRoute>
    afterFiles: Array<ManifestRewriteRoute>
    fallback: Array<ManifestRewriteRoute>
  }
  headers: Array<ManifestHeaderRoute>
  onMatchHeaders: Array<ManifestHeaderRoute>
  staticRoutes: Array<ManifestRoute>
  dynamicRoutes: ReadonlyArray<DynamicManifestRoute>
  dataRoutes: Array<ManifestDataRoute>

它是 next.config.jsrewrites / redirects / headers 的最终编译结果——把 :slug* 这种 path-to-regex 编译成实际正则、把 has / missing 转成可执行规则。

router-server.ts 用它做请求第一道路由处理:

incoming request
routes-manifest.redirects   ← 命中则 302
routes-manifest.rewrites.beforeFiles
filesystem check (public/, .next/static/)
routes-manifest.rewrites.afterFiles
matchers (app, pages, api, route)
routes-manifest.rewrites.fallback

生产排查提示:rewrite 不生效时,把 routes-manifest.json 打开搜 source pattern 看 regex 长成什么。常见坑:i18n locale 改变了实际正则。

4.9 next-font-manifest.json

next/font/googlenext/font/local 在 build 时把字体下载到 .next/static/media/ 并生成这份清单:

{
  "pages": {
    "/products/page": ["static/media/Inter.abc.woff2"]
  },
  "app": {
    /* App 路径同上 */
  },
  "pagesUsingSizeAdjust": false
}

消费端在 app-render 中插入 <link rel="preload" as="font" ...><head>,避免 FOIT。

4.10 react-loadable-manifest.json

老的 next/dynamic 实现(webpack chunkName 模式)用这份;新 RSC 模式下用 client-reference-manifest。但 Pages Router 中 next/dynamic 还是走它。

4.11 *.nft.json —— Node File Tracer

每个 entry 一份,结构极简:

{
  "version": 1,
  "files": [
    "../node_modules/some-pkg/lib/index.js",
    "../node_modules/some-pkg/data/locale.json"
  ]
}

next-trace-entrypoints-plugin + Vercel 开源的 @vercel/nft 产生。它记录"运行这个 entry 真正需要哪些文件",给 output: 'standalone' 做 tree-shaking-at-deploy。

第 28 讲与第 29 讲会展开。

4.12 dev 专用 manifest

_devPagesManifest.json / _devMiddlewareManifest.json / _clientMiddlewareManifest.js 由 dev server 在内存里维护、通过 /_next/static/... 暴露给客户端。on-demand entry 编译后立刻更新这几份,客户端 router 才知道某条新路由可用了。

5. 实操:动手解剖 .next/

5.1 准备 fixture

cd learning/nextjs-40-lectures/fixtures/lecture-06
pnpm install --ignore-workspace
pnpm build      # 一定要 build,不是 dev

dev 模式下大部分 manifest 在内存里,不落盘到 .next/。要看完整 manifest 必须 build。

5.2 速查表

ls .next/                       # 顶层
ls .next/server/                # 服务端产物(含大部分 manifest)
ls .next/server/app/            # App 路由产物,每个页面一个目录

挑几个最重要的看:

# 1. URL → 文件
cat .next/server/app-paths-manifest.json | jq

# 2. 反向映射
cat .next/server/app-path-routes-manifest.json | jq

# 3. App build manifest(chunks)
cat .next/app-build-manifest.json | jq '.pages | keys'

# 4. 一个具体 page 的 client-reference-manifest
cat .next/server/app/products/[id]/page_client-reference-manifest.js | head -60

# 5. Server Action manifest
cat .next/server/server-reference-manifest.json | jq '.encryptionKey, (.node | keys)'

# 6. 路由配置
cat .next/routes-manifest.json | jq '{redirects, rewrites: .rewrites.afterFiles, dynamicRoutes: .dynamicRoutes | length}'

# 7. Prerender
cat .next/prerender-manifest.json | jq '{routes: (.routes | keys), dynamicRoutes: (.dynamicRoutes | keys)}'

# 8. NFT trace
cat .next/server/app/products/[id]/page.js.nft.json | jq '.files | length'

5.3 一个完整的"断链"实验

目的:感受 manifest 缺失时框架行为。

  1. build 完成后,手动删除 .next/server/app-paths-manifest.json
  2. pnpm start
  3. 访问 /products——server 启动直接报错(Cannot find module 或 "Could not find a page"),因为 base-server 加载它失败。

恢复:删 .next/ 后再 pnpm build

5.4 主动改一个 page 看 manifest diff

  1. pnpm build,存档:cp -r .next /tmp/before-next
  2. 在 fixture 里加一个新 page:app/new-route/page.tsx
  3. pnpm build
  4. diff /tmp/before-next/server/app-paths-manifest.json .next/server/app-paths-manifest.json——你能看到新增的 key。
  5. 类似地 diff .next/server/app/new-route/page_client-reference-manifest.js —— 每个 page 都会有自己的一份。

6. 业务示例:自定义 cache handler 如何用 manifest

当你用 cacheHandler 接 Redis 时(详见第 30 讲),需要在 handler 里区分"哪条 URL 走静态、哪条走 ISR、revalidate 是多少"。这个判断完全依赖 prerender-manifest.json

参考源码:

src/server/route-modules/app-page/helpers/prerender-manifest-matcher.ts

这个 matcher 把 dynamicRoutes 编成正则数组,运行时用 match(pathname) 决定走哪条规则:

48:packages/next/src/server/route-modules/app-page/helpers/prerender-manifest-matcher.ts
export class PrerenderManifestMatcher {
  private readonly matchers: Array<Matcher>
  constructor(
    pathname: string,
    prerenderManifest: DeepReadonly<PrerenderManifest>
  ) {

你写自定义 cache handler 时不需要重新实现这个——通过 getRequestMeta(req, 'match') 就能拿到当前匹配结果,里面已经带了 prerender info。

7. 业务示例:CI 检查 manifest 完整性

生产事故里有一类"manifest 写漏"的 bug 很难本地复现(多发生在并行构建竞争)。可以加一条 CI 校验:

node -e '
const fs = require("fs");
const appPaths = JSON.parse(fs.readFileSync(".next/server/app-paths-manifest.json"));
const appPathRoutes = JSON.parse(fs.readFileSync(".next/server/app-path-routes-manifest.json"));

const missing = [];
for (const internalPath of Object.keys(appPaths)) {
  if (!(internalPath in appPathRoutes)) {
    missing.push(internalPath);
  }
}
if (missing.length) {
  console.error("Inconsistent app manifests:", missing);
  process.exit(1);
}
console.log("OK", Object.keys(appPaths).length, "entries match");
'

类似的可以校验:

  • app-paths-manifest.json 的每个 value 文件存在。
  • middleware-manifest.jsonfunctions 的每个 files 文件存在。
  • client-reference-manifest 中引用的 chunk 在 .next/static/chunks/ 下都能找到。

8. 重难点

8.1 同一份信息为什么会分散到多个 manifest?

  • 不同消费者关心不同切片(Edge runtime 只关心自己那部分 functions、不关心整个 build manifest)。
  • 不同生命周期:app-paths-manifest 在 build 时全量;prerender-manifest 等 generate-static-pages 阶段才能填齐;server-reference-manifest 等 client-entry-plugin emit 时填齐。
  • 历史包袱:Pages Router 与 App Router 都需要 build-manifest,分两份比合并更稳。

8.2 dev 与 prod 的"manifest 漂移"

dev 用 in-memory 增量更新、不全量落盘;prod 用一次性 build。常见 confusion:

现象真因
dev 工作,build 失败validateAppPaths 在 build 时跑、dev 不跑(有些校验只在 build 触发)
dev 行为与 prod 不一致prerender-manifest 在 dev 里不存在;force-static 在 dev 总是 dynamic
改 next.config.js 后 dev 不生效dev server 没监听 next.config.js,必须重启

8.3 Turbopack 与 webpack 的 manifest 差异

绝大多数 manifest 二者都生成,但有少量差异:

  • TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST = '_clientMiddlewareManifest.js' 是 Turbopack 专属。
  • TURBOPACK_CLIENT_BUILD_MANIFEST = 'client-build-manifest.json' 同上。
  • client-reference-manifest Turbopack 走自己的 react-server-dom-turbopack 而非 react-server-dom-webpack(详见 react vendoring skill)。

定位"为什么 webpack 行为正常 turbopack 异常"时,要单独对比这几份。

8.4 encryptionKey 是 Server Action 的"祖宗约定"

// server-reference-manifest.json
{ "encryptionKey": "<base64>" }

每次 build 随机,闭包变量加密用它。如果你部署完,用户带着旧客户端 JS 提交 action,server 用新 key 解密会失败。两种处置方式:

  1. 用户体验优先:next.config.js 固化 experimental.serverActions.encryption_key(多 build 共享)。
  2. 安全优先:每次 build 旋转 key,并强制客户端在新部署后刷新。

9. 检验问题

  1. app-paths-manifest.json 的 key 是 URL 吗?value 是相对什么的路径?
  2. client-reference-manifest 的 key 形如 "<file-path>#<export>",为什么需要带 export?
  3. dev 模式下能在 .next/ 看到完整 prerender-manifest.json 吗?为什么?
  4. routes-manifest.jsonrewrites 分为 3 段 (beforeFiles / afterFiles / fallback),三段分别在请求生命周期的什么时刻应用?
  5. server-reference-manifest.jsonencryptionKey 不变带来什么好处?变带来什么坏处?
  6. *.nft.json 是哪个 plugin 产生的?它在 output: 'standalone' 时怎么用?
  7. webpack 与 turbopack 在 manifest 上的差异主要在哪几份?
  8. fixture 里加一个新 page 后,至少有哪 5 个 manifest 会发生变化?
  9. middleware-manifest.jsonfunctionsmiddleware 字段有什么区别?
  10. 怎么用一行 jq 命令列出当前项目所有 dynamic ISR 路由?

10. 延伸阅读

  • packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts(800+ 行,建议完整阅读 ManifestNode 相关部分)
  • packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts(更长,看 ActionManifest 与 entry 整理)
  • packages/next/src/build/webpack/plugins/build-manifest-plugin-utils.ts
  • packages/next/src/server/load-manifest.external.ts(运行时怎么读 manifest 的)
  • 配套 fixture:learning/nextjs-40-lectures/fixtures/lecture-06/

下一讲预告

第 07 讲|Server / Client Components 的边界:我们将从 'use client' 这一行指令出发,拆 SWC transform、flight-client-entry-pluginclient-reference-manifest 三层,把 server / client 双 module graph 真正理顺,并用业务示例演示"上下文/Provider/Hooks 在 App Router 下的正确写法"。