ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Umi.js 资源预加载机制:从 dist 产物反推 preload_helper.js 的生成链路

2026/9/11 11:38:18 拓冰建站 浏览量
Umi.js 资源预加载机制:从 dist 产物反推 preload_helper.js 的生成链路 Umi.js 资源预加载机制:从 dist 产物反推 preload_helper.js 的生成链路【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umiUmi.js 生产构建的 dist 目录里,除了路由 chunk,还有一个很小的脚本 preload_helper.js。浏览器在 HTML 解析的极早期就会执行它,为当前路由的 JS 与 CSS 资源插入预加载标签,抢在路由组件挂载之前把资源拉下来。本文从这个最终产物反推整条机制:它由谁生成、内部的数据表如何采集、运行时又如何与打包器运行时协同,避免重复请求。产物反推:HTML head 里多出的一个 script 标签打开构建产物的 index.html,会在 head 看到一个指向{publicPath}/preload_helper.js的 script 标签(若 publicPath 配置为 auto,则按根路径处理);输出目录里同名文件与路由 chunk 放在一起。开启 hash 配置后,文件名会带一段由文件内容计算的 md5 前 8 位摘要作为后缀。这个脚本先于任何路由代码执行,职责只有两件事:判断当前路由该预加载哪些资源,以及把对应标签插入 head。还有两条线索值得注意:tern 微前端模式下,同一段逻辑不落盘成文件,而是把映射表以内联的script typeumi-route-chunk-files-mapJSON 写入 head;而整个生成逻辑统一收敛在 packages/preset-umi/src/features/routePreloadOnLoad/ 插件里,并不在 bundler 包中,stats 只是它的数据来源。映射表从何而来:onBuildComplete 解析 chunks 建立路由与资源映射preload_helper.js 本质是「一个模板 一张数据表」。数据表结构刻意压缩到四个字母字段:interface IRouteChunkFilesMap { p: string; // package.json 的 name,作为 data 属性前缀 b: string; // bundler 类型 f: [string, string | number][]; // [[文件名, chunkId]] r: Recordstring, number[]; // 路由路径 → f 的下标数组 }这张表在构建成功的 onBuildComplete 钩子中拼装,输入是构建 stats(webpack 走stats.toJson(),mako 走 compilation 对象)和 Umi 生成的路由树,过程分三步:遍历所有非 entry chunk,只挑出 .js 与 .css 文件,再看 chunk 的 origins(即它由哪个动态 import 引入)。只有指向生成的core/route.tsx的 origin 才算路由资源,入口 chunk 直接跳过;把每个路由文件的 import 请求经 alias 解析成绝对路径,并沿路由树向上收集父路由(layout、wrappers)的资源文件。route.wrappers 的实现会让 wrapper 路由与内层路由共享同一 absPath,此时保留索引列表更长的一方;r的键是路由 absPath,并按类 react-router 的评分算法排序:静态段 10、动态段 3、空段 1、通配段 -2,排序只影响遍历顺序,不影响匹配结果。触发条件同样写在插件的enableBy里:package.json 必须有 name(它是 data 属性前缀,缺了预加载标记就无从谈起),非 vite 模式、非 mpa 模式,且路由加载器为 esm(routeLoader.moduleType默认即 esm)。模板替换与产物落盘:占位符、terser 与 hash 文件名脚本主体来自 preloadRouteFilesScp.js 模板。文件本身就是手写的压缩风格代码,顶部注释明确提醒不要使用高级语法,以免转译器插入 helper 撑大脚本体积。模板里只有三个占位符:{{routeChunkFilesMap}}、{{basename}}、{{publicPath}}。publicPath 的替换有个细节:若开启了 runtimePublicPath,占位符会被替换成window.publicPath || ...的形式,保证运行时仍能动态取到基础路径。生产模式下,替换后的内容会再过一遍 terser 压缩(去注释、去 debugger),失败时降级使用未压缩版本并打印告警;随后 writeFileSync 写入输出目录,再由 addHTMLHeadScripts(stage: Infinity) 把 script 引用注入 head——第一节看到的那个标签就是这么来的。页面加载时预加载如何生效:路径匹配与 data 属性浏览器执行 preload_helper.js 时的步骤很轻:从 location.pathname 去掉 basename 得到路由路径,base 不匹配则整体跳过,避免非本站路径误触发;先在r里查静态路由的精确命中,未命中才把动态路由键转成正则逐一尝试(/:id变/[^/],/*变/.);js 资源插入script src async,css 资源插入link relpreload asstyle;每个标签额外写入data-{b}属性,值为{包名}:{chunkId}。这个 data 属性是与打包器运行时的协同约定:webpack 加载 chunk 时能凭它识别出已被预加载的脚本并直接复用,而不是对同一 chunk 再发一次请求。预加载因此是「一次下载、两次使用」,不构成浪费。一个容易误判的边界:mako 会把极小的异步 chunk 合并进入口,这类文件不会出现在映射表里,自然也没有预加载标签。这是正确行为,不是遗漏。适用边界:与 routePrefetch、clientLoader 的分工preload_helper.js 只解决「当前路由的资源文件」。它与另外两个预加载能力分工互补:routePrefetch(默认关闭)按用户意图(如鼠标悬停)提前预取即将跳转的路由,配置形如{ defaultPrefetch, defaultPrefetchTimeout };clientLoader 则把数据请求提到页面组件加载完成之前,解决瀑布流问题。三者分别覆盖进入页面、跳转前、数据依赖三个时点,配置上互不干扰。行动建议验证预加载链路是否生效,直接看产物:dist 里存在 preload_helper.js、index.html 的 head 有对应 script 标签,再在 Network 面板确认路由 chunk 早于路由组件挂载出现;若缺失,依次核对 package.json 的 name、esm 路由、非 mpa 三个启用条件。预加载的代价是带宽,而它对当前路由的资源是「全量」预取。对资源特别重的路由,用 clientLoader 与路由级拆分控制首屏体量,比期待预加载文件独力承担更现实。【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考