webpack 多入口 dependOn 依赖拆分实战:以 code-splitting-depend-on-advanced 示例剖析 entry 分层与 vendor 去重
本文以 examples/code-splitting-depend-on-advanced 示例为主线,系统讲解 webpack 通过入口对象(entry descriptor)的 dependOn 声明入口间依赖关系、把共享 vendor 与业务入口拆成独立 chunk 的做法。读完你会掌握 dependOn 的完整配置写法、它与 runtimeChunk: "single" 的分工、构建产物中 chunk 关联的实现形态,以及如何读懂 stats 里 <{…}>、>{…}<、={…}= 这些 chunk 关系符号。
场景:入口之间共享代码,既想去重又要保序
在多页应用(MPA)或多入口项目中,入口之间往往共享同一批第三方库。如果什么都不做,每个入口的 bundle 都会把共享依赖各打包一份,浪费带宽、破坏缓存。若简单地把它们抽成一个公共 chunk,又常常会遇到"依赖还没加载,入口代码就开始执行"的初始化顺序问题。
webpack 为此提供了对象形式(entry descriptor)的入口声明:除了 import,还能通过 dependOn 明确指出当前入口在加载前必须先行加载哪些入口。结合专门的 vendor 入口,即可做到:
- 共享依赖被单独提取、只打包一份;
- 加载入口 A 时,webpack 会先确保其依赖的 vendor 入口 chunk 已就绪再执行业务代码;
- 依赖关系可以传递与嵌套,形成清晰的入口分层。
一、示例配置全解:四个入口如何互相依赖
dependOn 的进阶示例配置文件位于 examples/code-splitting-depend-on-advanced/webpack.config.js,完整内容如下:
"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;
这里定义了四个入口,呈现两种入口写法:
| 入口名 | 写法 | 含义 |
|---|---|---|
app |
{ import: "./app.js", dependOn: ["other-vendors"] } |
对象写法,业务入口,依赖 vendor 入口 other-vendors |
page1 |
{ import: "./page1.js", dependOn: ["app", "react-vendors"] } |
对象写法,业务入口,同时依赖 app 与 react-vendors |
react-vendors |
["react", "react-dom", "prop-types"] |
数组写法,把 React 全家桶单独作为一个"供应商"入口 |
other-vendors |
"./other-vendors" |
字符串写法,引入 lodash、isomorphic-fetch 等另一批库 |
页面间依赖形成如下的"入口树":
react-vendors(react/react-dom/prop-types)
↑ dependOn
page1(page1.js)── dependOn ──→ app(app.js)── dependOn ──→ other-vendors(lodash / isomorphic-fetch)
↓
page1 还会触发异步加载 lazy_js(./lazy)
page1依赖app:意味着page1会在app之后执行,可复用app已经初始化好的代码;page1依赖react-vendors:react等库不会被塞进page1自身,而是作为前置 chunk 加载;app依赖other-vendors:app.js与page1.js里 import 的isomorphic-fetch、lodash都会"上提"到这个 vendor 入口,两个业务入口均不再重复包含它们。
此外还有两个配套的关键配置:
optimization.runtimeChunk: "single":把整套 webpack runtime 提取为单独一个runtime.jschunk。这样多个入口共享同一份运行时代码(模块缓存、__webpack_require__、chunk 加载逻辑等),注释中说明的理由是让文件在不同构建模式间保持一致;optimization.chunkIds: "named":chunk 使用可读名字(如app、page1、lazy_js)而非数字 id,便于跨构建对比产物;stats.chunks/stats.chunkRelations:构建后输出 chunk 及其父子/依赖关系,用于验证拆分结果。
dependOn 的官方类型定义
declarations/WebpackOptions.d.ts 中给出了精确语义:
The entrypoints that the current entrypoint depend on. They must be loaded when this entrypoint is loaded.
即:dependOn 列出的入口,在本入口被加载之前必须已经加载完毕。它既可以是单个入口名字符串,也可以是字符串数组(本示例用的是数组)。
二、业务源码:这些模块最终去了哪里
四个业务/供应商源文件完整罗列如下:
app.js(入口 app 的业务代码):
import isomorphicFetch from "isomorphic-fetch";
import lodash from "lodash";
console.log(isomorphicFetch, lodash);
page1.js(入口 page1 的业务代码,并异步引用 ./lazy):
import isomorphicFetch from "isomorphic-fetch";
import react from "react";
import reactDOM from "react-dom";
console.log(isomorphicFetch, react, reactDOM);
import("./lazy");
lazy.js(被 import() 异步加载的模块):
import lodash from "lodash";
import propTypes from "prop-types";
console.log(lodash, propTypes);
other-vendors.js(other-vendors 入口,充当第二组第三方库的容器,含附加初始化代码):
import lodash from "lodash";
import isomorphicFetch from "isomorphic-fetch";
// Additional initializations
console.log(lodash, isomorphicFetch);
注意模块归属的有趣之处:lodash 与 isomorphic-fetch 被 app.js、page1.js、other-vendors.js 同时引用,但示例把它们入口式地声明在 other-vendors,于是它们只归属 other-vendors chunk(模块 id 4、5),业务入口在运行时通过模块缓存直接使用;同理 react(id 0)、react-dom(id 1)、prop-types(id 2)只归属 react-vendors,即便 prop-types 仅被异步模块 lazy.js 用到,也统一收进 react-vendors 以最大化复用。lazy.js 本身则是一个独立的异步 chunk(lazy_js.js)。
三、源码视角:dependOn 是如何生效的
dependOn 不是魔法,从配置到产出要经过三处核心代码。
1) 入口描述被翻译成 EntryOptions
lib/EntryOptionPlugin.js 的 entryDescriptionToOptions 负责把入口描述对象转换为 EntryOptions,其中一行就是 dependOn: desc.dependOn,把它原样挂到每个入口的选项上。
2) dependOn 被规范化为数组
lib/config/normalization.js 里把用户写的字符串形式统一转成数组,保证后续代码无需区分写法:
dependOn:
value.dependOn &&
(Array.isArray(value.dependOn)
? value.dependOn
: [value.dependOn])
3) dependOn 影响 chunk 图的构建与 runtime
在 lib/ChunkGraph.js 中,dependOn 通过 Entrypoint 的父子关系(dependOn 指向的入口会成为当前入口的父 entrypoint)参与 chunk 图关系计算:当某个 entrypoint 是"被依赖方"且有子入口依赖它时,webpack 会把它与子入口的 runtime 依赖关系显式纳入考虑,保证运行时按正确顺序加载依赖 chunk。
从结构上看,这些入口描述最终会被 lib/EntryPlugin.js(经 lib/EntryOptionPlugin.js 分发)分别注册为 entry,随后在打包与生成阶段按 ChunkGraph 中的关系输出各自的 chunk 文件。
四、产物形态:chunk 关系如何落到 bundle 里
示例把默认输出(dist/)下的关键文件逐一声明了出来。理解它们,等于理解了 dependOn 的运行期效果。
runtime.js —— 唯一的 runtime chunk
由于开了 runtimeChunk: "single",所有运行时代码(模块缓存 __webpack_module_cache__、加载函数 __webpack_require__、异步 chunk 加载 __webpack_require__.e、脚本加载 __webpack_require__.l、JSONP 回调 webpackJsonpCallback 等)集中在 dist/runtime.js 对应的 runtime.js 中,并注册 "runtime": 0 表示自身已安装。它不再内嵌在任何一个入口文件里,因此被所有入口共享。
other-vendors.js / react-vendors.js —— 供应商 chunk
这两个 chunk 是 JSONP 数据块(不含 runtime),以 self["webpackChunk"].push(...) 形式登记模块:
other-vendors.js携带业务入口./other-vendors.js(模块 3)以及lodash(模块 4)、isomorphic-fetch(模块 5),末尾执行__webpack_exec__(3);react-vendors.js携带react(0)、react-dom(1)、prop-types(2),末尾依次__webpack_exec__(0, 1, 2)。
app.js 与 page1.js —— 依赖前置的业务入口
它们的关键在于内联的 startup 逻辑,使用了 __webpack_require__.O(chunk loaded runtime,负责"等待被依赖 chunk 加载完成后再执行本入口"):
- dist/app.js 对应的
app.js中:模块体只有对模块 5、4 的__webpack_require__(即isomorphic-fetch、lodash),并在末尾执行__webpack_require__.O(0, ["other-vendors"], () => __webpack_exec__(6))—— 明确声明执行 app 入口模块前,other-vendorschunk 必须加载完毕; - dist/page1.js 对应的
page1.js中:模块体对模块 5、0、1 做__webpack_require__,并执行__webpack_require__.e("lazy_js")触发异步加载./lazy;末尾是__webpack_require__.O(0, ["app", "react-vendors", "other-vendors"], () => __webpack_exec__(7)),即 page1 启动前必须等齐app、react-vendors(并经由传递关系加上other-vendors)。
这就是 dependOn 的运行期体现:业务入口不是一加载就立刻执行,而是先把自己 dependOn 的入口 chunk 全部"催熟",再运行自身模块。
lazy_js.js —— 异步 chunk
./lazy 被编译成 lazy_js.js(模块 8 放在 page1 引出的异步 chunk 中),只有当 page1 运行到 import("./lazy") 那一行时才经 __webpack_require__.e 拉取,实现按需加载。
五、构建统计解读:看懂 chunk 关联图
示例同时给出非优化与生产(-p,即 mode: production)两种模式的 stats 输出,验证 chunk 拆分符合预期。
非优化模式(Unoptimized)
资源(asset)与入口(entrypoint)概要:
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
可以看到:react-vendors 与 other-vendors 两个入口都以 runtime.js + 自身 chunk 的形式输出——这正是 runtimeChunk: "single" 与入口依赖结合的效果;而 app、page1 则只含自身业务代码(依赖已被前置)。chunk 间的关联用三条符号标注(这正是 stats.chunkRelations: true 的作用):
<{other-vendors}>:该 chunk 的前置依赖(parent)集合;>{page1}<:该 chunk 的异步子 chunk(import()出来的);={runtime}=:该 chunk 与 runtime chunk 互相依赖、平级关联。
据此还原各 chunk 关系:
chunk app.js (app):<{other-vendors}> <{runtime}> >{page1}<——app 依赖other-vendors与 runtime,又引出 page1;chunk page1.js (page1):<{app}> <{react-vendors}> <{runtime}> >{lazy_js}<——page1 的三个前置依赖与一个异步子 chunk 都一目了然;chunk react-vendors.js (react-vendors):={runtime}= >{page1}<;chunk other-vendors.js (other-vendors):={runtime}= >{app}<;chunk lazy_js.js:<{page1}>,由./page1.js 7:0-16的import()触发;chunk runtime.js (runtime):={other-vendors}= ={react-vendors}= >{app}< >{page1}<,是其它 chunk 的公共底座。
统计里还能看到模块级归属佐证:prop-types(31 bytes)在 react-vendors 下标注 from origin ./lazy.js,说明它虽只被懒加载模块使用,仍被收进 react-vendors;react、react-dom 的 origin 则是 ./page1.js。
生产模式(Production mode)
最小化后体积大幅下降(runtime.js 10.6 → 2.38 KiB,page1.js 1.86 → 271 bytes,app.js 1.41 → 193 bytes,other-vendors.js 2.11 → 232 bytes,react-vendors.js 1.3 → 197 bytes,lazy_js.js 1.1 → 157 bytes),而chunk 结构与关系完全保持不变(同样的 app → other-vendors、page1 → {app, react-vendors}、异步 lazy_js)。这正是配置里 chunkIds: "named" 注释"To keep filename consistent between different modes"要保证的可对比性。
六、与简单版的对比及工程建议
若想对比最简形态,可查看同主题的入门示例 examples/code-splitting-depend-on-simple/README.md:它只有 app 与 react-vendors 两个入口、单层依赖,且未启用 runtimeChunk: "single",因此 runtime 被打进 react-vendors.js(webpackBootstrap 引导块),app.js 则退化为纯 JSONP 数据块、不带独立 runtime。两个示例并列即可看出:
- 单层、少量入口:simple 版足矣,配置更精简;
- 多层嵌套、多组 vendor:进阶版把 runtime 独立成
runtime.js,配合dependOn串起的依赖链,任何入口的启动逻辑都只需"等待依赖 chunk",不再重复携带运行时代码,是最适合多页应用落地的形态。
实际工程中应用此模式还需留意几点:
dependOn只能指向同为入口的名字,不能指向任意 chunk;- 依赖图中不应出现循环依赖,否则构建会报错或产生错误顺序;
- 当同一库同时被多个"非依赖链"入口引用时,本示例手工划分 vendor 入口是"显式声明"思路;若希望完全自动化合并,可考虑 lib/optimize/SplitChunksPlugin.js 的自动拆包能力——但显式
dependOn的优势在于初始化顺序完全可控、构建输出可预期,二者可按需组合使用。
综上,通过 examples/code-splitting-depend-on-advanced 这样一个结构完整的可运行示例,你可以把"入口对象 + dependOn + 独立 vendor 入口 + runtimeChunk: single"这套组合直接复用到自己的多页应用:按页面拆入口、按依赖链分 vendor、把 runtime 抽离,最终在保证模块加载顺序正确的前提下,让共享依赖只下载一次、长期命中缓存。
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 StartedRust0624
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