SvelteKit 路由清单确定性修复:为什么构建 manifest 时必须排序目录条目
2026/9/20 14:28:49 网站建设 项目流程
  • Web框架
  • 后端
  • 前端

【免费下载链接】kit

web development, streamlined

项目地址:https://gitcode.com/gh_mirrors/kit/kit
点击查看免费下载

导读

本文基于 SvelteKit 仓库中的变更记录(.changeset/pre/sort-manifest-readdir.md)与对应源码实现,深入剖析一个看似微小、却直接影响 SSR 与客户端水合一致性的关键修复:构建路由清单时对readdirSync返回的目录条目强制排序。读完本文,你将理解 SvelteKit 路由清单(route manifest)如何生成、节点索引(node index)为何必须在不同运行时(Node 与 Bun 等)之间保持确定,以及一个.sort()调用如何避免水合错配与构建产物不稳定。

变更记录原文

该变更记录位于仓库的.changeset/pre/目录(预发布分支的变更集),全文如下:

--- '@sveltejs/kit': patch --- fix: sort directory entries when building the route manifest so node indices are deterministic across runtimes (e.g. Bun and Node)

这是一条标准 Changesets 格式的变更集:'@sveltejs/kit': patch声明该变更属于补丁级别(不破坏 API),fix:前缀表明这是一项缺陷修复。本次修复的目标是:在构建路由清单时对目录条目排序,使得节点索引在不同运行时(如 Bun 和 Node)之间保持确定性

路由清单是什么:SvelteKit 的"路线地图"

在 SvelteKit 中,src/routes目录下的每个+page.svelte+layout.svelte+error.svelte+page.server.js等文件都会被编译为应用的"路由清单"(manifest)。这份清单是路由系统的中枢,它回答两个核心问题:

  1. 一个 URL 应该匹配到哪条路由;
  2. 这条路由由哪些布局、页面、错误组件组成。

清单的生成入口是 packages/kit/src/core/sync/create_manifest_data/index.js,它被 packages/kit/src/core/sync/sync.js 中的all_typescreate等函数调用。生成的清单随后被写入磁盘,供两个场景消费:

  • 客户端清单:由 write_client_manifest.js 写入${outDir}/generated/${is_build ? 'build' : 'dev'}/client,用于驱动前端路由导航与组件懒加载(() => import('./nodes/${i}'));
  • 服务端清单:由write_server写入,用于 SSR 渲染时解析路由与加载数据。

节点索引:清单里的"身份证号"

在 create_manifest_data/index.js 中,所有路由组件会被收拢进一个nodes数组:

// populate the page nodes list // we do layouts/errors first as they are more likely to be reused, // and smaller indexes take fewer bytes. also, this guarantees that // the default error/layout are 0/1 for (const route of routes) { if (route.layout) { ... nodes.push(route.layout); } if (route.error) nodes.push(route.error); } for (const route of routes) { if (route.leaf) nodes.push(route.leaf); } const indexes = new Map(nodes.map((node, i) => [node, i]));

这里的nodes数组下标i就是节点索引(node index),它是组件在清单中的"身份证号":

  • 在服务端清单中,路由通过page.layoutspage.errorspage.leaf这三个索引数组引用节点(见 index.js#L434-L441);
  • 在客户端清单中,write_client_manifest用相同下标生成nodes/${i}.js模块,并写出路由字典(dictionary),将每个路由映射到一组索引(见 write_client_manifest.js#L45-L55)。

也就是说,同一个组件在服务端与客户端清单里必须拥有相同的索引,否则双方对"路由 3 由节点 [0,1,7] 组成"的理解就会不一致,直接导致水合(hydration)时组件树不匹配。

问题根源:readdirSync的返回顺序不保证

节点数组的填充顺序取决于清单构建时的目录遍历顺序——即fs.readdirSync返回条目(文件名)的顺序。而问题恰恰出在这里:

readdirSyncorder is not guaranteed and differs between runtimes (e.g. Node returns entries alphabetically, Bun in directory order).

不同运行时的readdirSync返回顺序并不一致:

  • Node.js:通常按字母序返回条目;
  • Bun:按其底层目录结构(inode/目录项顺序)返回,可能不是字母序。

如果构建时不做任何排序,那么同一个src/routes目录在 Node 下和 Bun 下可能产生不同的遍历顺序,进而产生不同的nodes数组顺序、不同的节点索引。这会带来两类问题:

  1. 跨运行时的不确定性:同一份代码在 Node 上构建、在 Bun 上运行(或反之)时,服务端清单与客户端清单可能不一致,引发水合错配;
  2. 构建产物不稳定:即使在同一运行时,readdirSync顺序在文档层面也不做保证,升级文件系统、系统库或运行时版本都可能导致产物内容"跳动"。

修复方式:一行.sort()

本次修复非常简洁,位于 create_manifest_data/index.js#L223-L229:

// We can't use withFileTypes because of a NodeJs bug which returns wrong results // with isDirectory() in case of symlinks: https://github.com/nodejs/node/issues/30646 // We sort the entries because `readdirSync` order is not guaranteed and differs // between runtimes (e.g. Node returns entries alphabetically, Bun in directory // order). Node indices are assigned from this traversal order, so without sorting // the SSR and client manifests can disagree, causing hydration mismatches. const files = fs .readdirSync(dir) .sort() .map((name) => ({ is_dir: fs.statSync(path.join(dir, name)).isDirectory(), name }));

要点拆解:

  • walk递归遍历每个路由目录时,先对readdirSync(dir)的结果调用.sort()(默认按 UTF-16 码元升序),再进入后续的文件分类与子目录递归(见 index.js#L361-L366,子目录递归同样遍历这份已排序的files);
  • 注释还解释了为什么不用withFileTypes:Node 存在一个与符号链接相关的 bug(nodejs/node#30646),在符号链接上isDirectory()可能返回错误结果,因此这里仍然用readdirSync().sort()+statSync的组合;
  • 由于节点索引由遍历顺序决定,排序后同一目录在任何运行时都会以相同顺序产出nodes数组,SSR 清单与客户端清单因此必然一致。

测试佐证:模拟"逆序运行时"

该修复并非仅靠注释自证,配套的单测直接模拟了一个返回逆序条目的运行时,验证输出与正常(排序)运行完全一致。测试位于 packages/kit/src/core/sync/create_manifest_data/index.spec.js#L107-L130:

test('assigns deterministic node indices regardless of readdirSync order', () => { // `readdirSync` order is not guaranteed and differs between runtimes (e.g. Node // returns entries alphabetically, Bun in directory order). Node indices are assigned // from the traversal order, so an unsorted result could make the SSR and client // manifests disagree. Simulate a runtime that returns entries in reverse order and // assert the output matches the normal (sorted) run. const expected = create('samples/basic'); const actual_readdir = fs.readdirSync; const spy = vi.spyOn(fs, 'readdirSync').mockImplementation((...args) => { const result = /** @type {string[]} */ ( /** @type {unknown} */ (actual_readdir(.../** @type {[any, any]} */ (args))) ); return /** @type {any} */ ([...result].sort().reverse()); }); try { const actual = create('samples/basic'); expect(actual.nodes.map(simplify_node)).toEqual(expected.nodes.map(simplify_node)); expect(actual.routes.map(simplify_route)).toEqual(expected.routes.map(simplify_route)); } finally { spy.mockRestore(); } });

这个测试的思路非常直观:

  1. 先用正常(已排序)的readdirSync构建samples/basic,得到期望输出expected
  2. 再用vi.spyOnfs.readdirSync替换为"返回逆序"的实现([...result].sort().reverse()),模拟 Bun 等不以字母序返回条目的运行时;
  3. 断言此时构建出的nodesroutesexpected完全一致,且用finally保证还原 spy。

除此之外,同文件还通过sort_routes对最终路由列表做排序(sort.js 定义、index.spec.js#L231-L272 用乱序输入验证输出稳定),进一步保证清单中"路由顺序"层面也具有确定性。

为什么这对开发者重要

这项修复对普通 SvelteKit 应用开发者最直接的价值体现在几个场景:

  • Bun 运行时下的一致性:SvelteKit 官方提供 adapter-bun,在开发(vite dev)或构建(vite build)阶段使用 Bun 运行时是受支持的用法。如果开发环境用 Node、生产环境用 Bun(或反之),排序保证了构建出的清单在两端语义完全一致,杜绝"本地好好的、部署到 Bun 就水合报错"的诡异现象;
  • 水合稳定性:SSR 与客户端各自独立解析清单,索引一旦错位,浏览器端hydrate时会出现Mismatch警告甚至 DOM 重建。排序从源头消除了这类竞态;
  • 可复现构建:清单生成不再依赖文件系统返回顺序,pnpm build的产物在相同源码下保持字节级稳定,便于缓存、差分对比与增量部署。

变更集与发布流程

该变更集位于.changeset/pre/目录,属于**预发布(pre-release)**变更集。仓库的.changeset/config.jsonbaseBranchversion-3,即该变更集面向 SvelteKit 3 的预发布周期。@changesets/changelog-github会在发布时自动将fix:摘要并入 CHANGELOG(packages/kit/CHANGELOG.md)。由于'@sveltejs/kit': patch标注为补丁级,该修复会随下一次补丁发布无感落地,不涉及任何 API 破坏,用户无需改动业务代码。

小结

一句话总结本次修复:在遍历src/routes目录构建清单之前,对readdirSync的返回结果调用.sort(),让节点索引不再依赖运行时(Node / Bun 等)的文件系统枚举顺序,从而保证 SSR 与客户端清单的一致性、消除水合错配风险,并让构建产物具备可复现性。它改动虽小,却是一个典型的"跨运行时确定性"工程问题,配合index.spec.js中的逆序模拟测试,完整覆盖了问题根源、修复手段与回归保障三个环节。

如果你想深入验证,可以查看 create_manifest_data/index.js 的walkpopulate逻辑、write_client_manifest.js 的索引消费方式,以及 index.spec.js 中的确定性测试。

  • Web框架
  • 后端
  • 前端

【免费下载链接】kit

web development, streamlined

项目地址:https://gitcode.com/gh_mirrors/kit/kit
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询