news 2026/9/8 22:39:50

webpack 示例解读:common-chunk-grandchildren——如何把深层异步祖先之间共享的模块拆成独立公共 Chunk

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 的直接子级,而是分布在一条跨越了多级异步加载关系的依赖链上。

在该示例的依赖关系里:

  • pageApageB是入口点通过require.ensure动态加载的;
  • pageCpageA同时 require 了reusableComponent
  • pageB动态加载了pageC

也就是说,reusableComponentpageA直接依赖、被pageC直接依赖,而pageC自身又是通过pageB间接(更深一层)加载进来的。于是reusableComponent成为这两个分处不同深度分支上的「共同祖先依赖」,webpack 会把它抽成独立的公共 chunk,pageApageC所在 chunk 都从该公共 chunk 中按需加载它。

示例文件结构

目录 examples/common-chunk-grandchildren 下包含如下文件:

文件作用
example.js入口模块,动态加载pageApageB
pageA.js异步页面 A,直接 requirereusableComponent
pageB.js异步页面 B,动态加载pageC
pageC.js异步页面 C,直接 requirereusableComponent
reusableComponent.jspageApageC共享的公共组件
webpack.config.js示例构建配置
template.md示例文档模板
README.md由模板渲染生成的最终文档

需要说明的是,template.md 使用了_{{example.js}}__{{dist/output.js}}__{{stdout}}__{{production:stdout}}_这类占位宏,构建脚本会把真实源码、打包产物与控制台输出回填进去,生成最终的可读文档 README.md。宏替换的逻辑集中在 examples/template-common.js(replaceResults/replaceBase等函数),按目录执行node build.js即可触发(见 examples/README.md 的 “Building an Example” 一节)。因此下文所述的产物细节均以渲染后的 README.md 为准。

四个模块的源码与依赖关系

先完整看一遍参与构建的源码。

入口 example.js:

var 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因此会按需加载pageApageB两个 chunk。

pageA.js:

var reusableComponent = require("./reusableComponent"); module.exports = function() { console.log("Page A"); reusableComponent(); };

pageB.js:

module.exports = function() { console.log("Page B"); require.ensure([], ()=>{ const page = require("./pageC"); page(); }); };

pageC.js:

var reusableComponent = require("./reusableComponent"); module.exports = function() { console.log("Page C"); reusableComponent(); };

reusableComponent.js:

module.exports = function() { console.log("reusable Component"); };

把依赖关系画成图(箭头表示require/require.ensure):

example.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降到 0,webpack 会认为拆分公共模块不划算,从而不执行切分。注释也明确提示“本示例太小了,实践中可使用默认值”。
  • 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 打包。

构建产物:一共五个文件 / Chunk

template.md 明确指出本次构建输出5 个文件(chunk),其概念布局如下:

  • 入口 chunk(output.js包含:
    • 模块系统(module system)
    • chunk 加载逻辑(chunk loading logic)
    • 入口点模块example.js
  • 四个附加 chunk,分别承载reusableComponentpageBpageApageC各一个模块。

由于配置了chunkIds: "named",渲染后的真实产物文件名不再是0.output.js1.output.js这种数字序号(template 中的0.output.js~3.output.js描述的是未启用命名时的通用形态),而是以所含源模块命名的四个异步 chunk:

文件(位于dist/承载模块
output.js(入口 chunk,name: main)运行时代码 +example.js
pageA_js.output.jspageA.js
pageB_js.output.jspageB.js
pageC_js.output.jspageC.js
reusableComponent_js.output.jsreusableComponent.js(split chunk)

可以看到reusableComponent确实被单独拆出,同时从pageApageB(因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.jsreusableComponent_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_jspageA_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最有趣:它本身只含模块 3(pageB.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_jspageB_jspageC_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 会打印构建统计。示例文档同时记录了Unoptimized(development)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在此场景下的行为准则:

  1. 共享判定基于模块图而非源码位置reusableComponent之所以被抽离,不是因为它在目录里长什么样,而是因为它被pageApageC两个以上异步 chunk 同时引用。
  2. 默认缓存组(default cache group)承担抽取动作。统计输出中的split chunk (cache group: default)即来源,相关实现位于 lib/optimize/SplitChunksPlugin.js。
  3. minSize是抽取的经济性闸门。默认约 20 KB 的阈值对于这个总计几百字节的微型示例来说过于苛刻,必须minSize: 0才会切分;在真实项目中应使用默认值让 webpack 自行权衡,避免拆分出大量毫无收益的小 chunk。
  4. 跨 chunk 的依赖会被自动补全为并行加载。产物中反复出现的Promise.all([e("reusableComponent_js"), e("pageX_js")])说明:当一个异步模块依赖位于另一 chunk 的模块时,webpack 会自动把两个 chunk 一起按需加载,无需开发者手动编排加载顺序。
  5. 模块只注册一次、全图共享。被抽出的模块在其专属 chunk 加载时写入__webpack_modules__,之后所有引用方都经由__webpack_require__(模块id)+ 模块缓存取得同一实例,从机制上杜绝了“多份拷贝”与循环加载冲突。

如果希望对比「公共模块位于入口同级异步分支」的切分形态,可以进一步阅读仓库中的姊妹示例 examples/common-chunk-and-vendor-chunk(入口级公共/第三方 chunk 切分)与 examples/code-splitting-depend-on-simple(entry 间显式依赖共享)。

在本仓库中运行该示例

官方示例的运行流程见 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 22:39:16

基于JSP+Servlet的会议室预约系统设计与实现

简介&#xff1a;基于JAVA/JSP技术打造的会议室预约系统&#xff0c;面向企业办公场景&#xff0c;用于解决会议室资源冲突、预约流程混乱等问题。系统分为管理员与员工两类角色&#xff1a;管理员可维护部门、员工、会议室信息并发布公告&#xff0c;员工可查看公告、在线预订…

作者头像 李华
网站建设 2026/9/8 22:39:07

毒化Windows环境下用CMake与vcpkg编译audio.cpp的完整实践

说起来有点好笑&#xff0c;我最近刚好在一台“年久失修”的Windows工作站上折腾audio.cpp的编译。所谓“年久失修”&#xff0c;不是机器硬件不行&#xff0c;而是这台机器的开发环境早就被各种历史遗留污染得不成样子&#xff1a;PATH里堆着三个不同版本的CMake&#xff0c;系…

作者头像 李华
网站建设 2026/9/8 22:38:52

Ruby on Rails 中的 Action View 完全指南:模板、局部模板与布局

Ruby on Rails 中的 Action View 完全指南&#xff1a;模板、局部模板与布局 【免费下载链接】rails Ruby on Rails 项目地址: https://gitcode.com/GitHub_Trending/rai/rails Action View 是 Ruby on Rails 中 MVC 架构的"V"&#xff0c;负责把控制器准备好…

作者头像 李华
网站建设 2026/9/8 22:36:20

C#医院电子病历系统源码解析与二次开发实战指南

简介&#xff1a;一份基于C#的医院电子病历系统源码包&#xff0c;面向需要完成毕业设计或从事医疗信息系统开发的C#学习者&#xff0c;针对患者信息管理、病历记录、医生排班、药品追踪与报表统计等典型业务场景提供可直接借鉴的实现方案。压缩包整体约197.15MB&#xff0c;适…

作者头像 李华
网站建设 2026/9/8 22:36:04

Linux下Qt串口通信实战:从环境配置到粘包处理全解析

简介&#xff1a;面向需要在 Linux 环境下进行设备串口通信开发的 Qt 程序员&#xff0c;这套示例包以实际可编译的 Qt 工程为主线&#xff0c;讲解如何基于 /dev/ttySx 串口和 QSerialPort 模块完成端口配置、数据读写与事件处理。资源共 23 个文件&#xff0c;主要包含 cpp/h…

作者头像 李华