webpack 代码分割进阶:用 entry.dependOn 搭建多入口共享依赖链,从配置到产物逐字节拆解
导读
本文围绕 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.js
page1 在依赖 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 + 页面入口顺序加载"的代码分割骨架。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00