webpack 示例解读:common-chunk-grandchildren——如何把深层异步祖先之间共享的模块拆成独立公共 Chunk
webpack 示例解读common-chunk-grandchildren——如何把深层异步祖先之间共享的模块拆成独立公共 Chunk【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack本指南聚焦 webpack 官方示例examples/common-chunk-grandchildren。该示例演示了当两个「动态加载的页面」处于不同的异步层级、却又都依赖同一个公共组件时webpack 如何借助splitChunks把该组件从深层模块图中抽离为独立的公共 chunk避免重复打包。读完本文你将理解require.ensure多级动态加载的依赖图形态、splitChunks公共 chunk 的切分逻辑以及chunkIds: named下异步 chunk 的命名与模块编号规律并会自行跑通该示例并读懂其打包产物与构建统计。示例要说明的问题examples/common-chunk-grandchildren的核心命题在 template.md 开篇就点明This example illustrates how common modules from deep ancestors of an entry point can be split into a separate common chunk.翻译过来就是从入口点出发的「深层祖先」之间共享的公共模块如何被拆进一个独立的公共 chunk。这里的“深层祖先”指的是共享代码并不存在于同一个入口 chunk 的直接子级而是分布在一条跨越了多级异步加载关系的依赖链上。在该示例的依赖关系里pageA与pageB是入口点通过require.ensure动态加载的pageC与pageA同时 require 了reusableComponentpageB又动态加载了pageC。也就是说reusableComponent被pageA直接依赖、被pageC直接依赖而pageC自身又是通过pageB间接更深一层加载进来的。于是reusableComponent成为这两个分处不同深度分支上的「共同祖先依赖」webpack 会把它抽成独立的公共 chunkpageA和pageC所在 chunk 都从该公共 chunk 中按需加载它。示例文件结构目录 examples/common-chunk-grandchildren 下包含如下文件文件作用example.js入口模块动态加载pageA、pageBpageA.js异步页面 A直接 requirereusableComponentpageB.js异步页面 B动态加载pageCpageC.js异步页面 C直接 requirereusableComponentreusableComponent.js被pageA与pageC共享的公共组件webpack.config.js示例构建配置template.md示例文档模板README.md由模板渲染生成的最终文档需要说明的是template.md 使用了_{{example.js}}_、_{{dist/output.js}}_、_{{stdout}}_、_{{production:stdout}}_这类占位宏构建脚本会把真实源码、打包产物与控制台输出回填进去生成最终的可读文档 README.md。宏替换的逻辑集中在 examples/template-common.jsreplaceResults/replaceBase等函数按目录执行node build.js即可触发见 examples/README.md 的 “Building an Example” 一节。因此下文所述的产物细节均以渲染后的 README.md 为准。四个模块的源码与依赖关系先完整看一遍参与构建的源码。入口 example.jsvar main function() { console.log(Main class); require.ensure([], () { const page require(./pageA); page(); }); require.ensure([], () { const page require(./pageB); page(); }); }; main();这里使用的require.ensure([], callback)是 webpack 经典的「代码分割 API」——把回调中同步require到的模块及其依赖单独打包成一个异步 chunk仅在回调执行时才去加载。example.js因此会按需加载pageA与pageB两个 chunk。pageA.jsvar reusableComponent require(./reusableComponent); module.exports function() { console.log(Page A); reusableComponent(); };pageB.jsmodule.exports function() { console.log(Page B); require.ensure([], (){ const page require(./pageC); page(); }); };pageC.jsvar reusableComponent require(./reusableComponent); module.exports function() { console.log(Page C); reusableComponent(); };reusableComponent.jsmodule.exports function() { console.log(reusable Component); };把依赖关系画成图箭头表示require/require.ensureexample.js (入口) / \ require.ensure require.ensure / \ pageA.js pageB.js | \ require reusableComponent require.ensure (再深一层) | \ (共享模块) pageC.js | require reusableComponent注意reusableComponent同时挂在pageA入口的第 1 层异步和pageC经由pageB的第 2 层异步之下。它在模块图中的「共同最低祖先」是入口模块因此既不能塞进pageAchunk否则pageC会重复一份也不能塞进pageCchunk否则pageA得不到。webpack 的做法是让它自成一个公共 chunk双方各自在需要时按需并行加载。配置文件解析示例的 webpack.config.js 非常精简却包含了让整个示例成立的两个关键开关use strict; const path require(path); /** type {import(webpack).Configuration} */ const config { // mode: development || production, entry: { main: [./example.js] }, optimization: { splitChunks: { minSize: 0 // This example is too small, in practice you can use the defaults }, chunkIds: named // To keep filename consistent between different modes (for example building only) }, output: { path: path.resolve(__dirname, dist), filename: output.js } }; module.exports config;逐项说明entry.main [./example.js]把入口 chunk 命名为main。打包后入口文件名由output.filename决定即dist/output.js。optimization.splitChunks.minSize 0这是触发公共 chunk 拆分的关键配置。splitChunks的默认minSize约为 20 KB未压缩包体而reusableComponent只有几十字节远低于默认阈值。如果不把minSize降到 0webpack 会认为拆分公共模块不划算从而不执行切分。注释也明确提示“本示例太小了实践中可使用默认值”。optimization.chunkIds named让异步 chunk 使用可读的名字而非自增数字 id。这样在 development 与 production 两种模式或多次构建下产出的文件名稳定一致便于对比产物差异。output.path/output.filename产物输出到示例目录下的dist/入口 chunk 命名为output.js。注意dist目录并不会被提交进仓库它是运行时构建生成的因此示例文档中展示的dist/*.js内容实际由 examples/template-common.js 在文档渲染时按占位宏读取真实构建产物回填。从配置即可推出本示例的构建命令本质上是以main为入口、开启splitChunksminSize: 0、以named方式为 chunk 命名的一次 webpack 打包。构建产物一共五个文件 / Chunktemplate.md 明确指出本次构建输出5 个文件chunk其概念布局如下入口 chunkoutput.js包含模块系统module systemchunk 加载逻辑chunk loading logic入口点模块example.js四个附加 chunk分别承载reusableComponent、pageB、pageA、pageC各一个模块。由于配置了chunkIds: named渲染后的真实产物文件名不再是0.output.js、1.output.js这种数字序号template 中的0.output.js~3.output.js描述的是未启用命名时的通用形态而是以所含源模块命名的四个异步 chunk文件位于dist/承载模块output.js入口 chunkname: main运行时代码 example.jspageA_js.output.jspageA.jspageB_js.output.jspageB.jspageC_js.output.jspageC.jsreusableComponent_js.output.jsreusableComponent.jssplit chunk可以看到reusableComponent确实被单独拆出同时从pageA、pageB因pageC依赖它的按需加载路径中剥离。整个公共 chunk 抽取在 webpack 中由 lib/optimize/SplitChunksPlugin.js 这一优化阶段插件负责optimization.splitChunks配置最终进入该插件的算法来决定哪些模块该归属哪个 cache group、是否达到拆分条件。产物剖析一入口 chunk 中的运行时代码dist/output.js是唯一带运行时的 chunk。其整体被一个 IIFE 包裹(() { ... })()开头是__webpack_modules__空模块表紧接着注入模块缓存与核心加载函数/******/ // The module cache /******/ const __webpack_module_cache__ {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ const cachedModule __webpack_module_cache__[moduleId]; /******/ if (cachedModule ! undefined) { /******/ return cachedModule.exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ const module __webpack_module_cache__[moduleId] { /******/ exports: {} /******/ }; /******/ __webpack_modules__moduleId; /******/ return module.exports; /******/ }随后注入的是这次示例用到的关键运行时片段/* webpack/runtime/ensure chunk */定义__webpack_require__.f与__webpack_require__.e(chunkId)。e会遍历f上的所有加载器并收集 Promise是“按需加载一个 chunk”的总入口/******/ __webpack_require__.e (chunkId) { /******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) { /******/ __webpack_require__.fkey; /******/ return promises; /******/ }, [])); /******/ };/* webpack/runtime/get javascript chunk filename */由chunkId .output.js拼出异步 chunk 的 URL这就是pageA_js.output.js、reusableComponent_js.output.js之类文件名的来源/******/ __webpack_require__.u (chunkId) (chunkId .output.js);/* webpack/runtime/load script */实现__webpack_require__.l(url, done, key, chunkId)通过动态创建script标签并挂到document.head来加载异步 chunk同时处理去重、超时120000 ms与错误清理避免在 IE 中产生内存泄漏。/* webpack/runtime/publicPath */__webpack_require__.p dist/表示异步 chunk 都相对dist/目录请求。/* webpack/runtime/jsonp chunk loading */这是异步 chunk 真正“落地”的机制。它维护installedChunks表main: 0表示入口已就绪定义__webpack_require__.f.j用 JSONP 方式发起加载并安装webpackJsonpCallback处理全局数组self[webpackChunk]/******/ const chunkLoadingGlobal self[webpackChunk] self[webpackChunk] || []; /******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); /******/ chunkLoadingGlobal.push webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));入口 chunk 末尾就是编译后的业务代码可以看到require.ensure被降级为「先并行加载好所需 chunk再执行回调」的形式var main function() { console.log(Main class); Promise.all(/*! require.ensure */[__webpack_require__.e(reusableComponent_js), __webpack_require__.e(pageA_js)]).then((() { const page __webpack_require__(/*! ./pageA */ 1); page(); }).bind(null, __webpack_require__))catch; __webpack_require__.e(/*! require.ensure */ pageB_js).then((() { const page __webpack_require__(/*! ./pageB */ 3); page(); }).bind(null, __webpack_require__))catch; }; main();这段产物信息量很大直接印证了公共 chunk 的设计require(./pageA)被编译成先Promise.all并行加载reusableComponent_js与pageA_js两个 chunk因为pageA.js依赖reusableComponent.js加载pageA前必须确保其依赖所在的公共 chunk 也已就绪require(./pageB)只加载pageB_js——此时pageB内部对pageC及其共享依赖reusableComponent的加载被推迟到pageB真正执行时才触发见下文的pageB_js.output.js。产物剖析二四个异步 chunk 与模块编号异步 chunk 采用 JSONP 推送格式(self[webpackChunk] self[webpackChunk] || []).push([[chunk名], {模块id: 模块函数}])。dist/reusableComponent_js.output.js独立的公共 chunk(self[webpackChunk] self[webpackChunk] || []).push([[reusableComponent_js],{ /***/ 2 /*!******************************!*\ !*** ./reusableComponent.js ***! \******************************/ /*! unknown exports (runtime-defined) */ /*! runtime requirements: module */ /*! CommonJS bailout: module.exports is used directly at 1:0-14 */ (module) { module.exports function() { console.log(reusable Component); }; /***/ } }]);dist/pageA_js.output.js第 0 号槽位被留空/* 0 */,pageA.js占模块 id 1并在内部通过__webpack_require__(2)引用公共模块reusableComponent(self[webpackChunk] self[webpackChunk] || []).push([[pageA_js],[ /* 0 */, /* 1 */ /*!******************!*\ !*** ./pageA.js ***! \******************/ /*! unknown exports (runtime-defined) */ /*! runtime requirements: module, __webpack_require__ */ /*! CommonJS bailout: module.exports is used directly at 3:0-14 */ /***/ ((module, __unused_webpack_exports, __webpack_require__) { var reusableComponent __webpack_require__(/*! ./reusableComponent */ 2); module.exports function() { console.log(Page A); reusableComponent(); }; /***/ }) ]]);dist/pageB_js.output.js最有趣它本身只含模块 3pageB.js但其中嵌套的require.ensure(./pageC)被编译为一次双 chunk 并行加载——既要reusableComponent_js因为pageC依赖它又要pageC_js(module, __unused_webpack_exports, __webpack_require__) { module.exports function() { console.log(Page B); Promise.all(/*! require.ensure */[__webpack_require__.e(reusableComponent_js), __webpack_require__.e(pageC_js)]).then(((){ const page __webpack_require__(/*! ./pageC */ 4); page(); }).bind(null, __webpack_require__))catch; }; /***/ }dist/pageC_js.output.js则与pageA_js结构类似模块 4 是pageC.js同样__webpack_require__(2)引用公共的reusableComponent。由此可以清晰地归纳出 webpack 的模块 id 规划入口运行时代码携带example.js异步模块pageA/reusableComponent/pageB/pageC依次取 1/2/3/4模块 2 跨越了pageA_js、pageB_js、pageC_js三个 chunk 的加载路径却只存在一份这正是splitChunks抽取公共 chunk 后整个运行体系仍然自洽的原因——公共模块函数只注册一次reusableComponent_js被 push 时写入__webpack_modules__后续任何 chunk 都能通过模块缓存命中同一实例。产物里/*! CommonJS bailout: module.exports is used directly at ... */与/*! unknown exports (runtime-defined) */注释则提示这些文件直接给module.exports赋值CJS 写法webpack 无法静态分析其导出形状属于常见提示而非错误。构建统计输出解读每次构建后 webpack 会打印构建统计。示例文档同时记录了Unoptimizeddevelopment与Production mode两套统计。Unoptimized未压缩asset output.js 8.78 KiB [emitted] (name: main) asset pageB_js.output.js 760 bytes [emitted] asset pageA_js.output.js 565 bytes [emitted] asset pageC_js.output.js 547 bytes [emitted] asset reusableComponent_js.output.js 441 bytes [emitted] chunk (runtime: main) output.js (main) 220 bytes (javascript) 4.83 KiB (runtime) [entry] [rendered] ./example.js main runtime modules 4.83 KiB 6 modules ./example.js 220 bytes [built] [code generated] [used exports unknown] entry ./example.js main chunk (runtime: main) pageA_js.output.js 136 bytes [rendered] ./example.js 3:1-6:3 ./pageA.js 136 bytes [built] [code generated] ... chunk (runtime: main) reusableComponent_js.output.js 69 bytes [rendered] split chunk (cache group: default) ./example.js 3:1-6:3 ./pageB.js 3:1-6:3 ./reusableComponent.js 69 bytes [built] [code generated] cjs require ./reusableComponent ./pageA.js 1:24-54 cjs require ./reusableComponent ./pageC.js 1:24-54 webpack X.X.X compiled successfully关键信息5 个 chunk 的构成与上文一致入口output.js中 220 字节业务代码example.js 4.83 KiB 运行时6 个 runtime modules。reusableComponent_js.output.js被标注为split chunk (cache group: default)即它由默认缓存组抽离而成引用它的位置被列出两处./example.js 3:1-6:3经由pageA与./pageB.js 3:1-6:3经由pageC直观展示了它作为深层公共祖先被两个异步分支共同引用的证据。由于四个模块都是 CJS 且入口以module.exports导出[used exports unknown]表示导出使用情况无法静态判定。统计中的webpack X.X.X为占位版本号构建脚本对模板输出做了归一化去掉具体版本与耗时信息见 examples/template-common.js 中的时间与路径替换规则。Production mode压缩asset output.js 1.85 KiB [emitted] [minimized] (name: main) asset pageB_js.output.js 228 bytes [emitted] [minimized] asset reusableComponent_js.output.js 141 bytes [emitted] [minimized] asset pageC_js.output.js 138 bytes [emitted] [minimized] asset pageA_js.output.js 137 bytes [emitted] [minimized] chunk (runtime: main) output.js (main) 220 bytes (javascript) 4.83 KiB (runtime) [entry] [rendered] ... ./example.js 220 bytes [built] [code generated] [no exports used]对比可见chunk 划分与模块归属在两种模式下完全一致这正是配置中chunkIds: named想保证的稳定性区别仅是产物被[minimized]压缩——入口由 8.78 KiB 降到 1.85 KiB四个异步 chunk 各自被压到 137~228 字节example.js标记变为[no exports used]生产模式下导出标记更激进。这说明公共 chunk 的抽取发生在压缩之前的优化阶段压缩只缩小体积、不改变拆分结构。公共 chunk 拆分背后的机制要点结合本示例产物可以提炼出 webpacksplitChunks在此场景下的行为准则共享判定基于模块图而非源码位置。reusableComponent之所以被抽离不是因为它在目录里长什么样而是因为它被pageA与pageC两个以上异步 chunk 同时引用。默认缓存组default cache group承担抽取动作。统计输出中的split chunk (cache group: default)即来源相关实现位于 lib/optimize/SplitChunksPlugin.js。minSize是抽取的经济性闸门。默认约 20 KB 的阈值对于这个总计几百字节的微型示例来说过于苛刻必须minSize: 0才会切分在真实项目中应使用默认值让 webpack 自行权衡避免拆分出大量毫无收益的小 chunk。跨 chunk 的依赖会被自动补全为并行加载。产物中反复出现的Promise.all([e(reusableComponent_js), e(pageX_js)])说明当一个异步模块依赖位于另一 chunk 的模块时webpack 会自动把两个 chunk 一起按需加载无需开发者手动编排加载顺序。模块只注册一次、全图共享。被抽出的模块在其专属 chunk 加载时写入__webpack_modules__之后所有引用方都经由__webpack_require__(模块id) 模块缓存取得同一实例从机制上杜绝了“多份拷贝”与循环加载冲突。如果希望对比「公共模块位于入口同级异步分支」的切分形态可以进一步阅读仓库中的姊妹示例 examples/common-chunk-and-vendor-chunk入口级公共/第三方 chunk 切分与 examples/code-splitting-depend-on-simpleentry 间显式依赖共享。在本仓库中运行该示例官方示例的运行流程见 examples/README.md 的 “Building an Example” 一节配合示例目录内已有的webpack.config.js步骤如下# 1. 在仓库根目录安装依赖 yarn # 2. 运行 setup生成测试/示例所需的脚手架 yarn setup # 3. 安装 webpack-cli开发依赖 yarn add --dev webpack-cli # 4. 进入示例目录并构建 cd examples/common-chunk-grandchildren node build.js构建会在示例目录下产出dist/你可以在命令行观察与上文一致的 chunk 统计信息。若要一次性重建所有示例可在根目录执行npm run build:examples对应脚本 examples/buildAll.js它会遍历 examples/examples.js 扫描出的所有含template.md的示例目录逐一构建。由于仓库只读请把构建输出视为学习验证过程无需改动任何仓库文件。小结examples/common-chunk-grandchildren用不足百行的代码把 webpack 拆包体系里最容易让人困惑的一个问题讲清楚了公共依赖并不一定长在相邻的兄弟节点上它可能横跨多级异步加载但只要它在多个 chunk 间共享splitChunks就能在优化阶段把它抽成独立 chunk并通过运行时的 JSONP 加载与模块级缓存让所有分支安全地共享同一份代码。入口 chunk 中Promise.all([e(reusableComponent_js), e(pageX_js)])的产物形态、split chunk (cache group: default)的统计标注以及 production 压缩后依然稳定的拆分结构都是理解这一机制的最佳注解。【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考