首页
/ webpack 多入口 dependOn 依赖拆分实战:以 code-splitting-depend-on-advanced 示例剖析 entry 分层与 vendor 去重

webpack 多入口 dependOn 依赖拆分实战:以 code-splitting-depend-on-advanced 示例剖析 entry 分层与 vendor 去重

2026-09-07 11:44:53作者:平淮齐Percy

本文以 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"] } 对象写法,业务入口,同时依赖 appreact-vendors
react-vendors ["react", "react-dom", "prop-types"] 数组写法,把 React 全家桶单独作为一个"供应商"入口
other-vendors "./other-vendors" 字符串写法,引入 lodashisomorphic-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-vendorsreact 等库不会被塞进 page1 自身,而是作为前置 chunk 加载;
  • app 依赖 other-vendorsapp.jspage1.js 里 import 的 isomorphic-fetchlodash 都会"上提"到这个 vendor 入口,两个业务入口均不再重复包含它们。

此外还有两个配套的关键配置:

  • optimization.runtimeChunk: "single":把整套 webpack runtime 提取为单独一个 runtime.js chunk。这样多个入口共享同一份运行时代码(模块缓存、__webpack_require__、chunk 加载逻辑等),注释中说明的理由是让文件在不同构建模式间保持一致;
  • optimization.chunkIds: "named":chunk 使用可读名字(如 apppage1lazy_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.jsother-vendors 入口,充当第二组第三方库的容器,含附加初始化代码):

import lodash from "lodash";
import isomorphicFetch from "isomorphic-fetch";

// Additional initializations
console.log(lodash, isomorphicFetch);

注意模块归属的有趣之处:lodashisomorphic-fetchapp.jspage1.jsother-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.jsentryDescriptionToOptions 负责把入口描述对象转换为 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-fetchlodash),并在末尾执行 __webpack_require__.O(0, ["other-vendors"], () => __webpack_exec__(6)) —— 明确声明执行 app 入口模块前,other-vendors chunk 必须加载完毕
  • 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 启动前必须等齐 appreact-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-vendorsother-vendors 两个入口都以 runtime.js + 自身 chunk 的形式输出——这正是 runtimeChunk: "single" 与入口依赖结合的效果;而 apppage1 则只含自身业务代码(依赖已被前置)。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-16import() 触发;
  • chunk runtime.js (runtime)={other-vendors}= ={react-vendors}= >{app}< >{page1}<,是其它 chunk 的公共底座。

统计里还能看到模块级归属佐证:prop-types(31 bytes)在 react-vendors 下标注 from origin ./lazy.js,说明它虽只被懒加载模块使用,仍被收进 react-vendorsreactreact-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-vendorspage1 → {app, react-vendors}、异步 lazy_js)。这正是配置里 chunkIds: "named" 注释"To keep filename consistent between different modes"要保证的可对比性。

六、与简单版的对比及工程建议

若想对比最简形态,可查看同主题的入门示例 examples/code-splitting-depend-on-simple/README.md:它只有 appreact-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 抽离,最终在保证模块加载顺序正确的前提下,让共享依赖只下载一次、长期命中缓存。

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