
Web框架后端前端【免费下载链接】kitweb 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声明该变更属于补丁级别不破坏 APIfix:前缀表明这是一项缺陷修复。本次修复的目标是在构建路由清单时对目录条目排序使得节点索引在不同运行时如 Bun 和 Node之间保持确定性。路由清单是什么SvelteKit 的路线地图在 SvelteKit 中src/routes目录下的每个page.svelte、layout.svelte、error.svelte、page.server.js等文件都会被编译为应用的路由清单manifest。这份清单是路由系统的中枢它回答两个核心问题一个 URL 应该匹配到哪条路由这条路由由哪些布局、页面、错误组件组成。清单的生成入口是 packages/kit/src/core/sync/create_manifest_data/index.js它被 packages/kit/src/core/sync/sync.js 中的all_types、create等函数调用。生成的清单随后被写入磁盘供两个场景消费客户端清单由 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.layouts、page.errors、page.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数组顺序、不同的节点索引。这会带来两类问题跨运行时的不确定性同一份代码在 Node 上构建、在 Bun 上运行或反之时服务端清单与客户端清单可能不一致引发水合错配构建产物不稳定即使在同一运行时readdirSync顺序在文档层面也不做保证升级文件系统、系统库或运行时版本都可能导致产物内容跳动。修复方式一行.sort()本次修复非常简洁位于 create_manifest_data/index.js#L223-L229// We cant 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注释还解释了为什么不用withFileTypesNode 存在一个与符号链接相关的 bugnodejs/node#30646在符号链接上isDirectory()可能返回错误结果因此这里仍然用readdirSync().sort()statSync的组合由于节点索引由遍历顺序决定排序后同一目录在任何运行时都会以相同顺序产出nodes数组SSR 清单与客户端清单因此必然一致。测试佐证模拟逆序运行时该修复并非仅靠注释自证配套的单测直接模拟了一个返回逆序条目的运行时验证输出与正常排序运行完全一致。测试位于 packages/kit/src/core/sync/create_manifest_data/index.spec.js#L107-L130test(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(); } });这个测试的思路非常直观先用正常已排序的readdirSync构建samples/basic得到期望输出expected再用vi.spyOn将fs.readdirSync替换为返回逆序的实现[...result].sort().reverse()模拟 Bun 等不以字母序返回条目的运行时断言此时构建出的nodes与routes与expected完全一致且用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.json中baseBranch为version-3即该变更集面向 SvelteKit 3 的预发布周期。changesets/changelog-github会在发布时自动将fix:摘要并入 CHANGELOGpackages/kit/CHANGELOG.md。由于sveltejs/kit: patch标注为补丁级该修复会随下一次补丁发布无感落地不涉及任何 API 破坏用户无需改动业务代码。小结一句话总结本次修复在遍历src/routes目录构建清单之前对readdirSync的返回结果调用.sort()让节点索引不再依赖运行时Node / Bun 等的文件系统枚举顺序从而保证 SSR 与客户端清单的一致性、消除水合错配风险并让构建产物具备可复现性。它改动虽小却是一个典型的跨运行时确定性工程问题配合index.spec.js中的逆序模拟测试完整覆盖了问题根源、修复手段与回归保障三个环节。如果你想深入验证可以查看 create_manifest_data/index.js 的walk与populate逻辑、write_client_manifest.js 的索引消费方式以及 index.spec.js 中的确定性测试。赞分享Web框架后端前端【免费下载链接】kitweb development, streamlined项目地址https://gitcode.com/gh_mirrors/kit/kit点击查看免费下载相关推荐为什么React Hooks必须无条件调用彻底理解Hooks规则的终极指南为什么React Hooks必须无条件调用彻底理解Hooks规则的终极指南 React Hooks彻底改变了函数组件的开发方式但许多开发者在使用时会遇到I前端Meteor 定时器 API 全解为什么必须用 Meteor.setTimeout / Meteor.setInterval 而非原生定时器Meteor 定时器 API 全解为什么必须用 Meteor.setTimeout / Meteor.setInterval 而非原生定时器 本篇指南围绕 d后端前端开发工具移动开发PHPExcel与PhpSpreadsheet性能对比为什么必须迁移PHPExcel与PhpSpreadsheet性能对比为什么必须迁移 在PHP的Excel处理领域 PHPExcel 曾经是无可争议的王者但这个项目在2后端数据处理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考