- 发布日期
第 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-manifest与server-reference-manifest—— 这两张表是 RSC 整套机制的"导线图"。
1. 为什么要有 Manifest?
App Router 的运行时(base-server / app-render)在请求进来时需要回答四类问题:
- 「这个 URL 对应哪个文件?」 →
app-paths-manifest.json - 「这个 page 渲染需要加载哪些 chunk?」 →
build-manifest.json/app-build-manifest.json - 「这个 client 组件在 RSC payload 中怎么引用?」 →
client-reference-manifest(核心) - 「这个 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 的文件名都集中起来,建议你把这一段当作 必背单词表:
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.js、server-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"
}
注意三件事:
- key 用 normalized internal path(带
/page//route后缀、保留 route group 与 parallel slot)。这是 framework internal pathname,不是 URL。要拿到 URL 还得过normalizeAppPath。 - value 是相对
.next/server/的 .js 路径——这是route-modules/app-page实际require()的目标。 - 同时存在 node 和 edge runtime 时,会有
app-paths-manifest.json和对应 edge 版本。
产生者:pages-manifest-plugin.ts,文件里同时累积 pages 与 app,最后吐两份:
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.ts 在 getAppPathsManifest() 中 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 用的:
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。
类型:
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 |
id | webpack module ID(数字或字符串,给 react-server-dom-webpack 用) |
name | export 名字('default' / 命名 export) |
chunks | 这个 module 需要的 JS / CSS chunk 文件名列表 |
async | 是否含 async module(如 dynamic ESM) |
rscModuleMapping 与 edgeRscModuleMapping 用来把 server graph 中的 module id 映射到 client graph 中的 client modules——这是 RSC 实现"在 server 渲染时引用 client 组件占位符"的关键。
消费场景:当 server 渲染流到一个 client component(即 'use client' 模块)时:
- RSC 写出占位符(
$L)+ 引用 id。 - 客户端通过
client-reference-manifest找到 id 对应的 chunk URL。 - 客户端
<script>动态加载该 chunk 后接管这个位置。
第 17 讲会深入 Flight 协议。
生产排查提示:客户端报
Module not found而服务端正常 → 多半是client-reference-manifest写漏(典型场景:自定义 webpack loader 处理了'use client'但没向 plugin 注册)。
4.5 server-reference-manifest —— Server Actions 的入口表
类型:
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。
消费场景:
- 客户端 form 提交,请求体里带 action ID。
- 服务端从
server-reference-manifest找到 action ID 对应的 entry。 - 加载对应 worker chunk,调用真正的函数。
生产排查提示:升级 deploy 后老用户报 "Failed to find Server Action" → 一般是
encryptionKey变了。解决方案:粘贴上一次 build 的 key 到next.config.js的experimental.serverActions.encryption_key(详见 docs)。
4.6 middleware-manifest.json
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 里定义:
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 的列表 |
preview | Draft Mode 的预览 props(cookie name 等) |
生产排查提示:用户报 "为什么我的 ISR 不刷新?" → 先看这个 manifest,确认该路由在
routes里、revalidate不是false。
4.8 routes-manifest.json —— 完整路由配置
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.js 里 rewrites / 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/google 与 next/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 缺失时框架行为。
- build 完成后,手动删除
.next/server/app-paths-manifest.json。 - 跑
pnpm start。 - 访问
/products——server 启动直接报错(Cannot find module或 "Could not find a page"),因为base-server加载它失败。
恢复:删 .next/ 后再 pnpm build。
5.4 主动改一个 page 看 manifest diff
pnpm build,存档:cp -r .next /tmp/before-next。- 在 fixture 里加一个新 page:
app/new-route/page.tsx。 pnpm build。diff /tmp/before-next/server/app-paths-manifest.json .next/server/app-paths-manifest.json——你能看到新增的 key。- 类似地 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) 决定走哪条规则:
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.json里functions的每个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-manifestTurbopack 走自己的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 解密会失败。两种处置方式:
- 用户体验优先:
next.config.js固化experimental.serverActions.encryption_key(多 build 共享)。 - 安全优先:每次 build 旋转 key,并强制客户端在新部署后刷新。
9. 检验问题
app-paths-manifest.json的 key 是 URL 吗?value 是相对什么的路径?client-reference-manifest的 key 形如"<file-path>#<export>",为什么需要带 export?- dev 模式下能在
.next/看到完整prerender-manifest.json吗?为什么? routes-manifest.json里rewrites分为 3 段 (beforeFiles/afterFiles/fallback),三段分别在请求生命周期的什么时刻应用?server-reference-manifest.json的encryptionKey不变带来什么好处?变带来什么坏处?*.nft.json是哪个 plugin 产生的?它在output: 'standalone'时怎么用?- webpack 与 turbopack 在 manifest 上的差异主要在哪几份?
- fixture 里加一个新 page 后,至少有哪 5 个 manifest 会发生变化?
middleware-manifest.json的functions与middleware字段有什么区别?- 怎么用一行 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.tspackages/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-plugin、client-reference-manifest 三层,把 server / client 双 module graph 真正理顺,并用业务示例演示"上下文/Provider/Hooks 在 App Router 下的正确写法"。