webpack 代码分割进阶:用 entry.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
导读
本文围绕 webpack 官方示例 examples/code-splitting-depend-on-advanced(其文档主体为 template.md,渲染结果见同目录 README.md),深入讲解多入口场景下如何利用entry的dependOn选项在入口点之间声明依赖关系,从而把 React、lodash 等共享第三方库抽成独立 vendor 分块、让多个页面入口按顺序加载并复用它们。读完本文你将掌握:entry描述对象的完整字段与dependOn语义(含链式依赖与传递闭包)、runtimeChunk: "single"与chunkIds: "named"的搭配用法,以及如何解读 stats 输出中的 chunk 关系符号(>{...}<、={...}=)和产物分块代码,理解底层调用链(lib/EntryOptionPlugin.js、lib/Entrypoint.js、lib/ChunkGraph.js)与官方 schema(schemas/WebpackOptions.json)对dependOn的完整定义。
一、示例场景:多页面应用如何共享 vendor
code-splitting-depend-on-advanced构建了一个典型的多页面(Multi-Page App)依赖网络,其中既有"业务入口依赖 vendor 入口",也有"页面入口依赖另一个页面入口"的链式关系。仓库中该示例共包含下列文件:
| 文件 | 角色 |
|---|---|
| webpack.config.js | 配置 4 个 entry 及 runtime / chunk 命名策略 |
| app.js | 业务入口一,使用isomorphic-fetch与lodash |
| page1.js | 业务入口二,使用react/react-dom,并动态import("./lazy") |
| lazy.js | 被 page1 懒加载的模块,依赖lodash与prop-types |
| other-vendors.js | 额外共享依赖入口(装载 lodash、isomorphic-fetch 并做附加初始化) |
模块依赖图如下:
react / react-dom / prop-types ──► react-vendors(vendor 入口) lodash / isomorphic-fetch ──────► other-vendors(vendor 入口) other-vendors ◄──dependOn── app.js app / react-vendors ◄──dependOn── page1.js ──► import() ──► lazy.jspage1在依赖react-vendors之外还依赖app,这就演示了dependOn的进阶(advanced)用法——被依赖的入口本身也可以是一个依赖了其他入口的业务入口,形成一条"other-vendors → app → page1"的加载顺序链。若把page1对app的依赖去掉,则退化为仓库中更简单的 code-splitting-depend-on-simple 场景,那里只有app → react-vendors一层依赖。
二、配置全解析:webpack.config.js
以下为 webpack.config.js 的完整内容(也是原文档 template.md 的核心配置段):
"use strict"; /** @type {import("webpack").Configuration} */ const config = { entry: { app: { import: "./app.js", dependOn: ["other-vendors"] }, page1: { import: "./page1.js", dependOn: ["app", "react-vendors"] }, "react-vendors": ["react", "react-dom", "prop-types"], "other-vendors": "./other-vendors" }, optimization: { runtimeChunk: "single", chunkIds: "named" // To keep filename consistent between different modes (for example building only) }, stats: { chunks: true, chunkRelations: true } }; module.exports = config;2.1 entry 描述对象的两种形态
这里同时展示了entry值的两种写法:
- 纯字符串/数组(如
"react-vendors": ["react", "react-dom", "prop-types"]、"other-vendors": "./other-vendors"):简写形态,等价于{ import: [...] }。 - 对象描述形态(如
app、page1):import声明入口模块,dependOn声明该入口依赖的其它入口名。
2.2 dependOn 的官方定义
schemas/WebpackOptions.json 对EntryDescription.dependOn的定义是:
"The entrypoints that the current entrypoint depend on. They must be loaded when this entrypoint is loaded."
即:当某个入口被加载时,它dependOn列出的那些入口点必须已经被加载。schema 允许两种形态——单个字符串或多个字符串组成的数组,且要求元素minLength: 1、uniqueItems: true(不能重复声明同一个入口)。同时required: ["import"]表明对象形态必须提供import。
同文件还定义了其余可用的入口描述字段,可作为实战参考:asyncChunks(是否允许生成按需加载的异步 chunk)、baseUri、chunkLoading、filename、library、layer、publicPath、runtime、wasmLoading、worker等(见 schemas/WebpackOptions.json)。
2.3 两个关键优化项的作用
optimization.runtimeChunk: "single":把 webpack 运行时(runtime)代码抽取为单独一个runtime.jschunk,避免每个入口都内联一份 bootstrap 代码。本示例中入口react-vendors与other-vendors的 Entrypoint 输出均为runtime.js + 自身 chunk(见后文 stats),而业务入口则通过dependOn间接复用该 runtime。optimization.chunkIds: "named":用可读名字而非数字编号作为 chunk 文件名(如lazy_js.js)。配置注释说明这是为了让不同模式(如仅构建 vs 生产)下产出文件名保持一致,便于在 README.md 中对照非优化与生产两次构建的产物。stats.chunks+stats.chunkRelations:在构建统计中输出每个 chunk 的关联关系(父 chunk、子 chunk、sibling 共享 chunk),便于观察dependOn生成的依赖图。
三、dependOn 的语义:顺序、链式与去重
3.1 入口代码回顾
四个源文件都非常薄,只是为了演示"哪些模块出现在哪个分块":
- app.js:
import isomorphicFetch from "isomorphic-fetch"; import lodash from "lodash"; - page1.js:额外引入
react、react-dom,并在末尾import("./lazy") - lazy.js:引入
lodash、prop-types - other-vendors.js:再次引入
lodash、isomorphic-fetch,注释 "Additional initializations" 表明其承担共享模块的副作用初始化职责
3.2 三类依赖关系产生的结果
① vendor 提取与跨入口去重:lodash、isomorphic-fetch同时被app、other-vendors使用,react、react-dom同时被page1、react-vendors使用。webpack 在把模块归属到 chunk 时,会把跨入口共享的第三方模块收敛到各自声明的 vendor 入口分块中,业务入口里不再重复打包这些模块,产物中app.js的模块数只有自身业务代码(调用__webpack_require__(5)、(4)指向 other-vendors 分块内模块)。
② 加载顺序保证:page1的代码只有在其全部前置依赖(app、react-vendors,进而传递到other-vendors与 runtime)加载完毕后才会执行,这正是后面产物代码中__webpack_require__.O(...)门控(gate)的由来。
③ 链式/传递依赖(Advanced 的核心):app依赖other-vendors,page1依赖app——最终page1就传递性地要求other-vendors先于自己加载。产物中 page1 的启动门控数组写的是["app","react-vendors","other-vendors"],即 webpack 已自动展开全部传递闭包,无需在 page1 里重复书写other-vendors。
此外lazy.js虽然静态引入lodash/prop-types,但它们分别已在other-vendors与react-vendors分块中存在,因此异步分块lazy_js中只装载./lazy.js自身,公共部分由运行时去复用既有模块缓存(模块缓存与__webpack_require__.e机制见下方产物剖析)。
3.3 源码级印证:dependOn 如何进入 Entrypoint
dependOn从配置到依赖图的关键链路(可从源码结构推断):
- lib/EntryOptionPlugin.js 在
entryOption阶段遍历每个 entry 名;若 value 是函数则走DynamicEntryPlugin,否则用EntryPlugin注册。 - lib/EntryOptionPlugin.js 的
entryDescriptionToOptions把入口描述对象中的name/filename/runtime/layer/dependOn/baseUri/...组装成EntryOptions,其中dependOn: desc.dependOn被原样透传。 - lib/Entrypoint.js 的
Entrypoint内部持有this._dependOn = new SortableSet(),通过addDependOn/dependOn(entrypoint)管理入口间的依赖集合(见 lib/Entrypoint.js)。 - 图构建阶段 lib/ChunkGraph.js 会依据
Entrypoint.dependOn关系建立 chunk 间边(注释中即写着// entryChunkA: dependOn = entryChunkB),从而约束初始 chunk 的加载顺序并计算 chunk 关系。
因此dependOn不是简单的"把代码并到一起",而是建立了带优先级的入口间依赖图,运行时才会据其生成门控加载逻辑。
四、构建产物逐文件剖析(dist/*.js)
示例在构建后产出 6 个文件(见 README.md 的dist/小节;本仓库中构建环境将 lodash/react 等第三方包解析为本地桩模块,module.exports = 'lodash'之类的短实现,便于离线复现,模块划分规律与实际 npm 包一致)。
4.1 dist/runtime.js —— 唯一的 webpack 运行时
runtime chunk 承载全部 bootstrap 代码:模块缓存__webpack_module_cache__、__webpack_require__系列函数,以及 chunk 加载用 runtime 模块:
chunk loaded(__webpack_require__.O):用于登记"等某批 chunk 就绪后执行回调"的门控。ensure chunk(__webpack_require__.e):Promise.all聚合各加载器。get javascript chunk filename(__webpack_require__.u):chunkId + ".js",这也是lazy_js.js命名的来源。jsonp chunk loading(__webpack_require__.f.j与全局self["webpackChunk"]):通过<script>标签异步加载分块,并处理加载失败(ChunkLoadError)与去重(installedChunks缓存)。
注意两处细节:
__webpack_require__.p = "dist/":所有异步请求都拼上dist/前缀,对应示例的输出目录。installedChunks初始含"runtime": 0,表示 runtime 自身永远"已安装",故不会被重复加载。
4.2 dist/other-vendors.js —— 共享依赖分块(lodash + isomorphic-fetch)
该分块内容含模块3(./other-vendors.js自身,带 "Additional initializations" 副作用)以及模块4(lodash)、5(isomorphic-fetch)。分块末尾的 runtime 段直接__webpack_exec__(3)执行入口模块——因为 vendor 入口没有前置依赖。
4.3 dist/react-vendors.js —— React 全家桶分块
含模块0(react)、1(react-dom)、2(prop-types),runtime 段执行__webpack_exec__(0), __webpack_exec__(1), __webpack_exec__(2),一次性初始化三个库。
4.4 dist/app.js —— 依赖门控的入口分块
app.js的业务模块引用的lodash(模块 4)与isomorphic-fetch(模块 5)都不在本分块内(对应位置留空/* 0 */, /* 1 */ ...),而在other-vendors分块中。其启动段是理解dependOn的关键:
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(moduleId)) /******/ __webpack_require__.O(0, ["other-vendors"], () => (__webpack_exec__(6))); /******/ var __webpack_exports__ = __webpack_require__.O();即:app 入口的业务代码(模块 6)只有当 chunk"other-vendors"已加载完成才会执行,这就是dependOn: ["other-vendors"]在产物中的直接体现——由 chunk-loaded runtime 保证 vendor 先就绪,避免业务代码执行时访问到尚未注册的模块。
4.5 dist/page1.js —— 传递依赖 + 动态懒加载的入口分块
page1.js的启动门控数组进一步体现了依赖闭包展开:
/******/ __webpack_require__.O(0, ["app","react-vendors","other-vendors"], () => (__webpack_exec__(7)));page1 配置只写了dependOn: ["app", "react-vendors"],但产物的就绪条件同时包含other-vendors——因为app又依赖other-vendors,webpack 自动把依赖链展开成完整闭包并按序等待。业务代码中懒加载调用为:
__webpack_require__.e(/*! import() */ "lazy_js").then(() => (__webpack_require__(/*! ./lazy */ 8)));先__webpack_require__.e("lazy_js")通过 JSONP 拉取异步分块,再执行其中的模块 8。
4.6 dist/lazy_js.js —— 只含自身业务代码的异步分块
由于lodash(模块 4)已在other-vendors、prop-types(模块 2)已在react-vendors中被实例化并缓存,lazy_js分块仅装载./lazy.js自身的模块。这展示了dependOn方案与动态import()叠加时的收益:懒加载分块不必重复携带大体积公共依赖,因为page1的依赖链保证它们在更早的阶段就绪。
五、stats 解读:用 chunkRelations 验证依赖图
配置中的stats.chunks/stats.chunkRelations会输出带关系符号的 chunk 列表(见 README.md 的 Info 节)。webpack 用尖括号描述关系:
A >{B}<:A 的子 chunk 是 B(B 由 A 派生/异步加载);A ={B}=:A 与 B 是兄弟(共享同一运行时/被同一组入口共享),此处即各入口 chunk 与runtime.js的关系;A <{B}>:A 的父 chunk 是 B(B 依赖先于 A 加载)。
摘录几行关键输出(Unoptimized 模式):
asset runtime.js 10.6 KiB [emitted] (name: runtime) asset other-vendors.js 2.11 KiB [emitted] (name: other-vendors) asset page1.js 1.86 KiB [emitted] (name: page1) asset app.js 1.41 KiB [emitted] (name: app) asset react-vendors.js 1.3 KiB [emitted] (name: react-vendors) asset lazy_js.js 1.1 KiB [emitted] Entrypoint app 1.41 KiB = app.js Entrypoint page1 1.86 KiB = page1.js Entrypoint react-vendors 11.9 KiB = runtime.js 10.6 KiB react-vendors.js 1.3 KiB Entrypoint other-vendors 12.7 KiB = runtime.js 10.6 KiB other-vendors.js 2.11 KiB chunk (runtime: runtime) app.js (app) 116 bytes <{other-vendors}> <{runtime}> >{page1}< [initial] [rendered] chunk (runtime: runtime) other-vendors.js (other-vendors) 210 bytes ={runtime}= >{app}< [initial] [rendered] chunk (runtime: runtime) page1.js (page1) 176 bytes <{app}> <{react-vendors}> <{runtime}> >{lazy_js}< [initial] [rendered] chunk (runtime: runtime) react-vendors.js (react-vendors) 87 bytes ={runtime}= >{page1}< [initial] [rendered]解读要点:
chunk app.js ... <{other-vendors}> <{runtime}> >{page1}<——app 的父 chunk 为 other-vendors 与 runtime;app 又是 page1 的父 chunk。other-vendors行dependent modules 64 bytes [dependent] 2 modules——lodash、isomorphic-fetch是被业务入口共享、因此被标记为 dependent 的模块。- vendor 入口
react-vendors、other-vendors的 Entrypoint 都会列出runtime.js,因为它们各自是入口点需要自带 runtime 引入;而app/page1的 Entrypoint 不重复列 runtime.js,其运行时就绪由dependOn依赖链(先加载 vendor 入口)保证。 (runtime: runtime)前缀说明所有初始 chunk 共享同一个 runtime chunk(对应runtimeChunk: "single");lazy_js.js无 entry 名、只被>{}<从 page1 指向,是纯异步 chunk。
将上述统计映射到页面加载场景:访问"react-vendors"页面时 HTML 注入runtime.js + react-vendors.js;访问 "other-vendors" 页面时注入runtime.js + other-vendors.js;访问 app 时注入runtime.js + other-vendors.js + app.js;访问 page1 时则注入runtime.js + react-vendors.js + other-vendors.js + app.js + page1.js,并可按需在触发懒加载后追加lazy_js.js。
六、Unoptimized 与 Production 构建对比
同一示例在非优化与生产模式下的产物体积形成鲜明对照(数据取自 README.md 的 Info 两节):
| 产物文件 | Unoptimized | Production(minimized) |
|---|---|---|
| runtime.js | 10.6 KiB | 2.38 KiB |
| other-vendors.js | 2.11 KiB | 232 B |
| page1.js | 1.86 KiB | 271 B |
| app.js | 1.41 KiB | 193 B |
| react-vendors.js | 1.3 KiB | 197 B |
| lazy_js.js | 1.1 KiB | 157 B |
生产模式下 runtime 由 10.6 KiB 压缩到 2.38 KiB,各业务与 vendor chunk 也大幅瘦身。两次构建的 chunk 划分与依赖关系完全一致(这正是配置里使用chunkIds: "named"的目的——模式间文件命名保持一致),说明dependOn划分的 chunk 结构在开发/生产之间是稳定的。
七、实战要点与边界(结合源码归纳)
- 依赖必须指向真实存在的入口名:
dependOn数组里写的是entry的键(如app、react-vendors),不能指向不存在的入口或模块路径。 - 适合"显式 vendor 入口"式拆分:当你想完全掌控 vendor 边界(例如
other-vendors还需执行额外初始化代码)时,dependOn比自动化的SplitChunks.cacheGroups更直接可预期;当依赖矩阵复杂、需要自动最小化共享分块时,SplitChunks仍是更省心的方案。 - 链式依赖会自动传递:从 dist/page1.js 中
["app","react-vendors","other-vendors"]的门控数组可以看到,无需在每层重复罗列所有祖先依赖,webpack 会在 ChunkGraph 阶段基于 Entrypoint 的_dependOn集合计算传递闭包(相关逻辑见 lib/ChunkGraph.js)。 - 共享模块只落一份:被多个入口引用的模块会收敛到依赖链上游的 vendor 分块,并以模块 id 引用方式被下游复用,避免业务分块重复打包;懒加载分块同样能复用已实例化的公共模块。
- 与 runtimeChunk 的配合:多入口 +
dependOn时通常配合runtimeChunk: "single"抽离公共运行时,否则每个 vendor 入口都可能内联 runtime 造成冗余;也可为入口单独指定runtime字段做更细粒度控制。 - 文件名可预期性:结合
chunkIds: "named"让异步分块(如lazy_js)与入口分块都有稳定的可读文件名,便于产物缓存策略与长期引用。
如果只需要一层"业务入口 → 共享 vendor"的简单拆分,可参考同仓库的简化版本 examples/code-splitting-depend-on-simple/webpack.config.js;本文所述的 advanced 版本在其之上加入"入口依赖另一个业务入口"与"懒加载复用上游公共模块"两层进阶能力。对照官方 schema(schemas/WebpackOptions.json)与入口解析实现(lib/EntryOptionPlugin.js),即可在自己的多页面工程中安全复刻这套"共享 vendor + 页面入口顺序加载"的代码分割骨架。
【免费下载链接】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),仅供参考