首页
/ webpack 代码分割进阶:用 entry.dependOn 搭建多入口共享依赖链,从配置到产物逐字节拆解

webpack 代码分割进阶:用 entry.dependOn 搭建多入口共享依赖链,从配置到产物逐字节拆解

2026-09-07 13:55:09作者:舒璇辛Bertina

导读

本文围绕 webpack 官方示例 examples/code-splitting-depend-on-advanced(其文档主体为 template.md,渲染结果见同目录 README.md),深入讲解多入口场景下如何利用 entrydependOn 选项在入口点之间声明依赖关系,从而把 React、lodash 等共享第三方库抽成独立 vendor 分块、让多个页面入口按顺序加载并复用它们。读完本文你将掌握:entry 描述对象的完整字段与 dependOn 语义(含链式依赖与传递闭包)、runtimeChunk: "single"chunkIds: "named" 的搭配用法,以及如何解读 stats 输出中的 chunk 关系符号(>{...}<={...}=)和产物分块代码,理解底层调用链(lib/EntryOptionPlugin.jslib/Entrypoint.jslib/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-fetchlodash
page1.js 业务入口二,使用 react/react-dom,并动态 import("./lazy")
lazy.js 被 page1 懒加载的模块,依赖 lodashprop-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.js

page1 在依赖 react-vendors 之外还依赖 app,这就演示了 dependOn进阶(advanced)用法——被依赖的入口本身也可以是一个依赖了其他入口的业务入口,形成一条"other-vendors → app → page1"的加载顺序链。若把 page1app 的依赖去掉,则退化为仓库中更简单的 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 值的两种写法:

  1. 纯字符串/数组(如 "react-vendors": ["react", "react-dom", "prop-types"]"other-vendors": "./other-vendors"):简写形态,等价于 { import: [...] }
  2. 对象描述形态(如 apppage1):import 声明入口模块,dependOn 声明该入口依赖的其它入口名。

2.2 dependOn 的官方定义

schemas/WebpackOptions.jsonEntryDescription.dependOn 的定义是:

"The entrypoints that the current entrypoint depend on. They must be loaded when this entrypoint is loaded."

即:当某个入口被加载时,它 dependOn 列出的那些入口点必须已经被加载。schema 允许两种形态——单个字符串或多个字符串组成的数组,且要求元素 minLength: 1uniqueItems: true(不能重复声明同一个入口)。同时 required: ["import"] 表明对象形态必须提供 import

同文件还定义了其余可用的入口描述字段,可作为实战参考:asyncChunks(是否允许生成按需加载的异步 chunk)、baseUrichunkLoadingfilenamelibrarylayerpublicPathruntimewasmLoadingworker 等(见 schemas/WebpackOptions.json)。

2.3 两个关键优化项的作用

  • optimization.runtimeChunk: "single":把 webpack 运行时(runtime)代码抽取为单独一个 runtime.js chunk,避免每个入口都内联一份 bootstrap 代码。本示例中入口 react-vendorsother-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.jsimport isomorphicFetch from "isomorphic-fetch"; import lodash from "lodash";
  • page1.js:额外引入 reactreact-dom,并在末尾 import("./lazy")
  • lazy.js:引入 lodashprop-types
  • other-vendors.js:再次引入 lodashisomorphic-fetch,注释 "Additional initializations" 表明其承担共享模块的副作用初始化职责

3.2 三类依赖关系产生的结果

① vendor 提取与跨入口去重lodashisomorphic-fetch 同时被 appother-vendors 使用,reactreact-dom 同时被 page1react-vendors 使用。webpack 在把模块归属到 chunk 时,会把跨入口共享的第三方模块收敛到各自声明的 vendor 入口分块中,业务入口里不再重复打包这些模块,产物中 app.js 的模块数只有自身业务代码(调用 __webpack_require__(5)(4) 指向 other-vendors 分块内模块)。

② 加载顺序保证page1 的代码只有在其全部前置依赖(appreact-vendors,进而传递到 other-vendors 与 runtime)加载完毕后才会执行,这正是后面产物代码中 __webpack_require__.O(...) 门控(gate)的由来。

③ 链式/传递依赖(Advanced 的核心)app 依赖 other-vendorspage1 依赖 app——最终 page1传递性地要求 other-vendors 先于自己加载。产物中 page1 的启动门控数组写的是 ["app","react-vendors","other-vendors"],即 webpack 已自动展开全部传递闭包,无需在 page1 里重复书写 other-vendors

此外 lazy.js 虽然静态引入 lodash/prop-types,但它们分别已在 other-vendorsreact-vendors 分块中存在,因此异步分块 lazy_js 中只装载 ./lazy.js 自身,公共部分由运行时去复用既有模块缓存(模块缓存与 __webpack_require__.e 机制见下方产物剖析)。

3.3 源码级印证:dependOn 如何进入 Entrypoint

dependOn 从配置到依赖图的关键链路(可从源码结构推断):

  1. lib/EntryOptionPlugin.jsentryOption 阶段遍历每个 entry 名;若 value 是函数则走 DynamicEntryPlugin,否则用 EntryPlugin 注册。
  2. lib/EntryOptionPlugin.jsentryDescriptionToOptions 把入口描述对象中的 name/filename/runtime/layer/dependOn/baseUri/... 组装成 EntryOptions,其中 dependOn: desc.dependOn 被原样透传。
  3. lib/Entrypoint.jsEntrypoint 内部持有 this._dependOn = new SortableSet(),通过 addDependOn/dependOn(entrypoint) 管理入口间的依赖集合(见 lib/Entrypoint.js)。
  4. 图构建阶段 lib/ChunkGraph.js 会依据 Entrypoint.dependOn 关系建立 chunk 间边(注释中即写着 // entryChunkA: dependOn = entryChunkB),从而约束初始 chunk 的加载顺序并计算 chunk 关系。

因此 dependOn 不是简单的"把代码并到一起",而是建立了带优先级的入口间依赖图,运行时才会据其生成门控加载逻辑。


四、构建产物逐文件剖析(dist/*.js)

示例在构建后产出 6 个文件(见 README.mddist/ 小节;本仓库中构建环境将 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-vendorsprop-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]

解读要点:

  1. chunk app.js ... <{other-vendors}> <{runtime}> >{page1}<——app 的父 chunk 为 other-vendors 与 runtime;app 又是 page1 的父 chunk。
  2. other-vendorsdependent modules 64 bytes [dependent] 2 modules——lodashisomorphic-fetch 是被业务入口共享、因此被标记为 dependent 的模块。
  3. vendor 入口 react-vendorsother-vendors 的 Entrypoint 都会列出 runtime.js,因为它们各自是入口点需要自带 runtime 引入;而 app/page1 的 Entrypoint 不重复列 runtime.js,其运行时就绪由 dependOn 依赖链(先加载 vendor 入口)保证。
  4. (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 结构在开发/生产之间是稳定的。


七、实战要点与边界(结合源码归纳)

  1. 依赖必须指向真实存在的入口名dependOn 数组里写的是 entry 的键(如 appreact-vendors),不能指向不存在的入口或模块路径。
  2. 适合"显式 vendor 入口"式拆分:当你想完全掌控 vendor 边界(例如 other-vendors 还需执行额外初始化代码)时,dependOn 比自动化的 SplitChunks.cacheGroups 更直接可预期;当依赖矩阵复杂、需要自动最小化共享分块时,SplitChunks 仍是更省心的方案。
  3. 链式依赖会自动传递:从 dist/page1.js["app","react-vendors","other-vendors"] 的门控数组可以看到,无需在每层重复罗列所有祖先依赖,webpack 会在 ChunkGraph 阶段基于 Entrypoint 的 _dependOn 集合计算传递闭包(相关逻辑见 lib/ChunkGraph.js)。
  4. 共享模块只落一份:被多个入口引用的模块会收敛到依赖链上游的 vendor 分块,并以模块 id 引用方式被下游复用,避免业务分块重复打包;懒加载分块同样能复用已实例化的公共模块。
  5. 与 runtimeChunk 的配合:多入口 + dependOn 时通常配合 runtimeChunk: "single" 抽离公共运行时,否则每个 vendor 入口都可能内联 runtime 造成冗余;也可为入口单独指定 runtime 字段做更细粒度控制。
  6. 文件名可预期性:结合 chunkIds: "named" 让异步分块(如 lazy_js)与入口分块都有稳定的可读文件名,便于产物缓存策略与长期引用。

如果只需要一层"业务入口 → 共享 vendor"的简单拆分,可参考同仓库的简化版本 examples/code-splitting-depend-on-simple/webpack.config.js;本文所述的 advanced 版本在其之上加入"入口依赖另一个业务入口"与"懒加载复用上游公共模块"两层进阶能力。对照官方 schema(schemas/WebpackOptions.json)与入口解析实现(lib/EntryOptionPlugin.js),即可在自己的多页面工程中安全复刻这套"共享 vendor + 页面入口顺序加载"的代码分割骨架。

登录后查看全文
热门项目推荐
相关项目推荐