webpack 代码分割实战:用 entry 的 dependOn 实现 vendor chunk 与 app chunk 的职责分离
webpack 代码分割实战用 entry 的 dependOn 实现 vendor chunk 与 app 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本文以仓库自带的示例 examples/code-splitting-depend-on-simple 为核心完整讲解 webpack 中通过entry配置项的dependOn选项做入口级代码分割的用法、产物结构带 runtime 的 vendors chunk 与纯 jsonp 推送的 app chunk以及 stats 输出中 chunk 关系箭头的含义读完你可以掌握 vendor 与应用代码分离打包的标准配置并能结合 webpack 源码理解dependOn在编译流程中的校验与 chunk 关联机制。示例目标一次构建得到两个各司其职的 chunk该示例要演示的是Code Splitting with entrypoint dependOn——即利用入口点之间的依赖关系让 webpack 把不同入口的模块打进不同文件并且由被依赖入口持有运行时runtime依赖方入口只携带自身模块代码。目录下的最小工程仅包含一个配置文件和一个入口文件examples/code-splitting-depend-on-simple/webpack.config.jsexamples/code-splitting-depend-on-simple/app.jswebpack.config.js配置两个入口并用 dependOn 建立依赖配置文件全文如下与示例目录中的文件一致use strict; /** type {import(webpack).Configuration} */ const config { entry: { app: { import: ./app.js, dependOn: [react-vendors] }, react-vendors: [react, react-dom, prop-types] }, optimization: { chunkIds: named // To keep filename consistent between different modes (for example building only) }, stats: { chunks: true, chunkRelations: true } }; module.exports config;逐项说明配置作用entry.app.import: ./app.js应用入口导入app.jsentry.app.dependOn: [react-vendors]声明app入口依赖react-vendors入口只要appchunk 被加载react-vendorschunk 必须已经在页面上。这是本示例的核心选项取值是入口名可以是字符串或字符串数组见后文类型定义entry[react-vendors]一个简写形式字符串数组等于三个并列的入口模块react、react-dom、prop-types。这些第三方库被钉在这个入口里从而形成一个独立的 vendor chunkoptimization.chunkIds: named使用基于模块名的 chunk id使不同构建模式development/production下文件名保持稳定便于本地缓存策略与构建产物对比stats.chunks: truestats.chunkRelations: true让构建日志输出 chunk 列表以及 chunk 之间的依赖/被依赖关系即下文 stats 中{...}与{...}箭头方便直观验证分割结果入口文件app.js的内容非常简单它引用了三个第三方库用来验证这些库确实被剥离进了 vendor chunkimport react from react; import reactDOM from react-dom; import propTypes from prop-types; console.log(react, reactDOM, propTypes);产物结构谁持有 runtime谁只做推送构建后得到两个文件dist/react-vendors.js和dist/app.js。两者在结构上有着本质区别这正是dependOn想要的效果。dist/app.js没有 runtime只通过 webpackChunk 全局数组推送模块app入口没有自己的 runtime因为dependOn的语义是借用被依赖入口的 runtime因此产物是一个 jsonp 式的全局数组push调用运行时由react-vendors.js中安装的回调接管use strict; (self[webpackChunk] self[webpackChunk] || []).push([[app],{ /***/ 3 /*!****************!*\ !*** ./app.js ***! \****************/ /*! namespace exports */ /*! exports [not provided] [no usage info] */ /*! runtime requirements: __webpack_require__, __webpack_require__.n, __webpack_require__.r, __webpack_exports__, __webpack_require__.* */ (__unused_webpack_module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ __webpack_require__(/*! react */ 0); /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1__ __webpack_require__(/*! react-dom */ 1); /* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1___default /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_2__ __webpack_require__(/*! prop-types */ 2); /* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_2___default /*#__PURE__*/__webpack_require__.n(prop_types__WEBPACK_IMPORTED_MODULE_2__); console.log((react__WEBPACK_IMPORTED_MODULE_0___default()), (react_dom__WEBPACK_IMPORTED_MODULE_1___default()), (prop_types__WEBPACK_IMPORTED_MODULE_2___default())); /***/ } }, /******/ __webpack_require__ { // webpackRuntimeModules /******/ var __webpack_exec__ (moduleId) (__webpack_require__(moduleId)) /******/ var __webpack_exports__ (__webpack_exec__(3)); /******/ } ]);可以观察到几个关键点首行(self[webpackChunk] self[webpackChunk] || []).push([[app], { ... }])chunk 名为app模块字典只有模块3即./app.js。react、react-dom、prop-types以模块 id0、1、2被__webpack_require__引用——它们并不在本 chunk 内而是由 vendor chunk 提供。末尾的webpackRuntimeModules只包含一个极小的启动函数拿到__webpack_require__后执行入口模块3并把 exports 交给宿主 runtime 的__webpack_require__.O机制见下文chunk loaded部分。dist/react-vendors.js携带完整 webpackBootstrap 的入口 chunkreact-vendors是被依赖方因此它承载了完整运行时。其模块部分依次包含三个第三方库模块示例仓库用node_modules下的桩文件模拟这三个包/******/ (() { // webpackBootstrap /******/ var __webpack_modules__ ([ /* 0 */ /*!*******************************!*\ !*** ./node_modules/react.js ***! \*******************************/ /*! unknown exports (runtime-defined) */ /*! runtime requirements: module */ /*! CommonJS bailout: module.exports is used directly at 1:0-14 */ /***/ ((module) { module.exports react; /***/ }), /* 1 */ /*!***********************************!*\ !*** ./node_modules/react-dom.js ***! \***********************************/ /*! unknown exports (runtime-defined) */ /*! runtime requirements: module */ /*! CommonJS bailout: module.exports is used directly at 1:0-14 */ /***/ ((module) { module.exports react-dom; /***/ }), /* 2 */ /*!************************************!*\ !*** ./node_modules/prop-types.js ***! \************************************/ /*! unknown exports (runtime-defined) */ /*! runtime requirements: module */ /*! CommonJS bailout: module.exports is used directly at 1:0-14 */ /***/ ((module) { module.exports prop-types; /***/ }) /******/ ]);紧随模块字典之后是 webpack 的运行时代码README 中折叠展示与dependOn直接相关的核心段如下。模块缓存与 require 函数/******/ // 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: {} /******/ }; /******/ // Execute the module function /******/ __webpack_modules__moduleId; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m __webpack_modules__;jsonp chunk 加载机制——这是app chunk 能靠 push 工作的宿主/******/ /* webpack/runtime/jsonp chunk loading */ /******/ (() { /******/ // no baseURI /******/ /******/ // object to store loaded and loading chunks /******/ // undefined chunk not loaded, null chunk preloaded/prefetched /******/ // [resolve, reject, Promise] chunk loading, 0 chunk loaded /******/ const installedChunks { /******/ react-vendors: 0 /******/ }; /******/ /******/ // no chunk on demand loading /******/ /******/ // no prefetching /******/ /******/ // no preloaded /******/ /******/ // no HMR /******/ /******/ // no HMR manifest /******/ /******/ __webpack_require__.O.j (chunkId) (installedChunks[chunkId] 0); /******/ /******/ // install a JSONP callback for chunk loading /******/ const webpackJsonpCallback (parentChunkLoadingFunction, data) { /******/ let [chunkIds, moreModules, runtime] data; /******/ // add moreModules to the modules object, /******/ // then flag all chunkIds as loaded and fire callback /******/ var moduleId, chunkId, i 0; /******/ if(chunkIds.some((id) (installedChunks[id] ! 0))) { /******/ for(moduleId in moreModules) { /******/ if(__webpack_require__.o(moreModules, moduleId)) { /******/ __webpack_require__.m[moduleId] moreModules[moduleId]; /******/ } /******/ } /******/ if(runtime) var result runtime(__webpack_require__); /******/ } /******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data); /******/ for(;i chunkIds.length; i) { /******/ chunkId chunkIds[i]; /******/ if(__webpack_require__.o(installedChunks, chunkId) installedChunks[chunkId]) { /******/ installedChunks[chunkId][0](); /******/ } /******/ installedChunks[chunkId] 0; /******/ } /******/ return __webpack_require__.O(result); /******/ } /******/ /******/ const chunkLoadingGlobal self[webpackChunk] self[webpackChunk] || []; /******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); /******/ chunkLoadingGlobal.push webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); /******/ })();注意installedChunks { react-vendors: 0 }中记录的是named chunk id因为配置了optimization.chunkIds: named值为0表示已加载。chunkLoadingGlobal.push被替换为webpackJsonpCallback之后dist/app.js里那次push([[app], {...}, runtime])就会触发回调把模块3注入__webpack_require__.m执行 app 自带的 runtime 模块标记app为已加载。其余 runtime 片段README 折叠区内包括 CJS 兼容的__webpack_require__.n取 default export、__webpack_require__.d定义 getter、__webpack_require__.ohasOwnProperty 快捷方法、__webpack_require__.r打__esModule标记以及顶层 await 支持所需的__webpack_require__.Ochunk loaded机制。最后以 startup 收尾/******/ // startup /******/ // Load entry module and return exports /******/ // This entry module is referenced by other modules so it cant be inlined /******/ __webpack_require__(0); /******/ __webpack_require__(1); /******/ let __webpack_exports__ __webpack_require__(2); /******/ __webpack_exports__ __webpack_require__.O(__webpack_exports__); /******/ })() ;这里能读出一条实用结论react/react-dom/prop-types是app入口的直接依赖启动阶段被显式__webpack_require__加载并导出说明 vendor 入口的模块会按入口模块顺序被初始化——vendor 文件必须在页面上先于或随同app 文件加载这正是dependOn语义在 HTML 引用顺序上的体现。源码剖析dependOn 在编译流程中做了什么dependOn的类型定义见 declarations/WebpackOptions.d.ts/** * The entrypoints that the current entrypoint depend on. They must be loaded when this entrypoint is loaded. */ dependOn?: string[] | string;即当前入口点所依赖的入口点列表加载当前入口点时它们必须已被加载。在 schemas/WebpackOptions.json 中同样有对应 schema 约束。第一步选项透传。lib/EntryOptionPlugin.js 负责把entry配置翻译成 EntryPlugin。applyEntryOption遍历entry对象的每个键调用entryDescriptionToOptions生成选项对象——dependOn在这里原样进入 options源码第 114 行dependOn: desc.dependOn随后为每个import模块创建EntryPlugin挂载到 compiler。也就是说dependOn是入口描述对象EntryDescription的一等公民字段与runtime、filename、layer、publicPath等并列。第二步chunk 图构建时的依赖建立与校验。核心逻辑在 lib/Compilation.js 的createChunkGroups流程中outer: for (const [name, { options: { dependOn, runtime } }] of this.entries) { if (dependOn runtime) { const err new WebpackError(Entrypoint ${name} has dependOn and runtime specified. This is not valid. Entrypoints that depend on other entrypoints do not have their own runtime. They will use the runtime(s) from referenced entrypoints instead. Remove the runtime option from the entrypoint.); ... this.errors.push(err); } if (dependOn) { const entry this.entrypoints.get(name); const referencedChunks entry .getEntrypointChunk() .getAllReferencedChunks(); for (const dep of dependOn) { const dependency this.entrypoints.get(dep); if (!dependency) { throw new Error( Entry ${name} depends on ${dep}, but this entry was not found ); } if (referencedChunks.has(dependency.getEntrypointChunk())) { const err new WebpackError( Entrypoints ${name} and ${dep} use dependOn to depend on each other in a circular way. ); ... continue outer; } entry.addDependOn(dependency); if (dependency.addChild(entry)) { entry.addParent(dependency); } } } ... }从源码结构可以读出三条硬性约束排错时非常有用dependOn与runtime互斥。依赖入口没有自己的 runtime借用被引用入口的 runtime所以同时配置两者会直接报错并提示移除runtime选项。这与产物中dist/app.js不含 bootstrap 代码完全对应。被依赖入口必须存在。this.entrypoints.get(dep)找不到时抛出Entry ${name} depends on ${dep}, but this entry was not found——写错入口名会立即失败而不是静默忽略。禁止循环依赖。若A dependOn B且 B 的 chunk 组已经引用了 A 的入口 chunk则报use dependOn to depend on each other in a circular way错误并跳过。建立依赖后调用entry.addDependOn(dependency)与dependency.addChild(entry)/entry.addParent(dependency)把两个入口点登记为父子关系。Entrypoint类的实现见 lib/Entrypoint.jsaddDependOn(entrypoint)将目标加入内部的SortableSetthis._dependOn.add(entrypoint)dependOn(entrypoint)方法提供成员判断。这个依赖集合会随 chunk 组参与后续的 chunk 图构建buildChunkGraph决定了哪些 chunk 必须被一起加载。另外值得一提的是 lib/Compilation.js 中runtime分支的错误提示如果把另一个入口点名字误用作runtime值webpack 会明确提示Did you mean to use dependOn: ... instead to allow using entrypoint ... within the runtime of ...?——这是区分dependOn复用某个入口的 runtime 与其模块环境与独立 runtime chunk 的最直接指引。构建日志解读stats 如何展示分割结果README 的 Info 部分给出了两种模式下完整的 stats 输出stats.chunkRelations: true开启后箭头表示 chunk 关系{x}表示依赖 x chunk{x}表示被 x 依赖。Unoptimizeddevelopmentasset react-vendors.js 7.4 KiB [emitted] (name: react-vendors) asset app.js 1.59 KiB [emitted] (name: app) chunk (runtime: react-vendors) app.js (app) 139 bytes {react-vendors} [initial] [rendered] ./app.js app ./app.js 139 bytes [built] [code generated] [no exports] [used exports unknown] entry ./app.js app chunk (runtime: react-vendors) react-vendors.js (react-vendors) 87 bytes (javascript) 3.25 KiB (runtime) {app} [entry] [rendered] prop-types react-vendors react react-vendors react-dom react-vendors runtime modules 3.25 KiB 6 modules cacheable modules 87 bytes ./node_modules/prop-types.js 31 bytes [built] [code generated] [used exports unknown] from origin ./app.js harmony side effect evaluation prop-types ./app.js 3:0-35 harmony import specifier prop-types ./app.js 5:29-38 cjs self exports reference ./node_modules/prop-types.js 1:0-14 entry prop-types react-vendors ./node_modules/react-dom.js 30 bytes [built] [code generated] [used exports unknown] from origin ./app.js harmony side effect evaluation react-dom ./app.js 2:0-33 harmony import specifier react-dom ./app.js 5:19-27 cjs self exports reference ./node_modules/react-dom.js 1:0-14 entry react-dom react-vendors ./node_modules/react.js 26 bytes [built] [code generated] [used exports unknown] from origin ./app.js harmony side effect evaluation react ./app.js 1:0-26 harmony import specifier react ./app.js 5:12-17 cjs self exports reference ./node_modules/react.js 1:0-14 entry react react-vendors webpack X.X.X compiled successfullyProduction modeasset react-vendors.js 1.2 KiB [emitted] [minimized] (name: react-vendors) asset app.js 180 bytes [emitted] [minimized] (name: app) chunk (runtime: react-vendors) app.js (app) 139 bytes {react-vendors} [initial] [rendered] ./app.js app ./app.js 139 bytes [built] [code generated] [no exports] [no exports used] entry ./app.js app chunk (runtime: react-vendors) react-vendors.js (react-vendors) 87 bytes (javascript) 3.05 KiB (runtime) {app} [entry] [rendered] prop-types react-vendors react react-vendors react-dom react-vendors runtime modules 3.05 KiB 5 modules cacheable modules 87 bytes ./node_modules/prop-types.js 31 bytes [built] [code generated] [used exports unknown] from origin ./app.js harmony side effect evaluation prop-types ./app.js 3:0-35 harmony import specifier prop-types ./app.js 5:29-38 cjs self exports reference ./node_modules/prop-types.js 1:0-14 entry prop-types react-vendors ./node_modules/react-dom.js 30 bytes [built] [code generated] [used exports unknown] from origin ./app.js harmony side effect evaluation react-dom ./app.js 2:0-33 harmony import specifier react-dom ./app.js 5:19-27 cjs self exports reference ./node_modules/react-dom.js 1:0-14 entry react-dom react-vendors ./node_modules/react.js 26 bytes [built] [code generated] [used exports unknown] from origin ./app.js harmony side effect evaluation react ./app.js 1:0-26 harmony import specifier react ./app.js 5:12-17 cjs self exports reference ./node_modules/react.js 1:0-14 entry react react-vendors webpack X.X.X compiled successfully对照产物日志可以这样解读chunk (runtime: react-vendors) app.js (app) ... {react-vendors}appchunk 的runtime 由react-vendors承担箭头{react-vendors}即dependOn建立的app 依赖 react-vendors关系。react-vendors.js ... {app}反向箭头表明app会被react-vendors加载链牵动(runtime)标记的 3.25 KiB / 3.05 KiB 就是上文 jsonp、chunk loaded 等运行时模块的体积production 下因代码更少从 6 个 runtime modules 减为 5 个。from origin ./app.js与各harmony import specifier行说明了三个 vendor 模块来自哪个模块的 import即 app.js 的import语句是它们被纳入react-vendors入口的原因——react-vendors入口的import列表由入口配置直接指定[react, react-dom, prop-types]而依赖来源信息仍指向发起方。体积对比development 下react-vendors.js7.4 KiB、app.js1.59 KiBproduction 压缩后分别为 1.2 KiB 与 180 bytes。应用代码139 字节的 app.js 源与 vendor 代码被清晰地切成了两个文件且 app 文件几乎不含运行时代码——这正是dependOn分割模式的目标形态。实战要点小结适用场景当你希望第三方库react、react-dom 这类与应用代码物理分离、共享同一份 runtime并且保证 vendor 文件一定先于应用文件加载时entry描述对象中的dependOn是最直接的声明方式相比SplitChunksPlugin自动抽取它的边界完全由入口配置决定结果可预期。加载契约dependOn表达的是加载当前入口时被依赖入口必须已加载。多入口站点中通常意味着 HTML 页面需按 vendor → app 的顺序引入脚本若希望更复杂的按需/多 vendor 结构可参考仓库中同一族示例 examples/code-splitting-depend-on-advanced在dependOn基础上加入了lazy、other-vendors等入口组合。常见错误均来自 lib/Compilation.js 的报错文本同时配置dependOn与runtimedependOn指向不存在的入口两个入口互相dependOn形成环。文件名稳定性生产环境若依赖固定文件名做缓存或 CDN 分发可沿用示例的optimization.chunkIds: named如需更强内容寻址可再叠加output.filename的[contenthash]。验证手段保留stats: { chunks: true, chunkRelations: true }构建后通过{...}/{...}箭头确认依赖方向再检查产物中 runtime 位于哪个文件本例中全部 runtime 在react-vendors.js。参考文件索引文件说明examples/code-splitting-depend-on-simple/README.md本示例原始文档配置、产物与 stats 记录examples/code-splitting-depend-on-simple/webpack.config.jsdependOn最小可运行配置examples/code-splitting-depend-on-simple/app.js应用入口lib/EntryOptionPlugin.js入口描述到 EntryPlugin 选项的转换dependOn在此透传lib/Entrypoint.jsEntrypoint.addDependOn/dependOn成员集合实现lib/Compilation.jschunk 组构建阶段对dependOn的校验、循环检测与父子关系建立declarations/WebpackOptions.d.tsEntryDescription.dependOn官方类型注释【免费下载链接】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),仅供参考