首页
/ webpack 代码分割进阶:用 entrypoint dependOn 实现 Vendor 与 App 共享运行时

webpack 代码分割进阶:用 entrypoint dependOn 实现 Vendor 与 App 共享运行时

2026-09-06 09:38:23作者:庞眉杨Will

本篇指南围绕 webpack 官方示例 examples/code-splitting-depend-on-simple 展开,讲解如何通过 entry.<name>.dependOn 选项把多个入口分成"依赖入口(如 react-vendors)+ 依赖它的入口(如 app)",让后者自动共享前者的运行时与模块缓存,从而避免同一份 vendor 代码在多个 bundle 中重复打包。读完本文,你将掌握 dependOn 的完整配置写法、构建产物的结构解读、stats 输出的逐行含义,以及从 EntryOptionPlugin.jsCompilation.js 的源码级实现链路,能够把它应用到多页面或"主应用 + 共享 vendor"的真实项目中。

示例目标:一个入口依赖另一个入口

示例目录 examples/code-splitting-depend-on-simple 演示的是 Code Splitting 中 entrypoint dependOn 的用法。核心思想是:

  • reactreact-domprop-types 等第三方库单独作为一个入口 react-vendors 打包;
  • 让业务入口 app 声明 dependOn: ["react-vendors"]
  • webpack 会保证加载 app 前必须先加载 react-vendors,并且 app.js 中不再重复携带这些第三方模块和运行时——它们全部由 react-vendors.js 提供。

这在多入口项目中尤其常见:如果 appAappB 两个入口都 import React,但没有 dependOn,webpack 无法确定该把 React 放到哪个 chunk(放 appA 会导致 appB 运行时找不到模块),最终要么复制多份要么报错。dependOn 就是显式告诉 webpack 加载顺序与运行时归属的官方手段。

完整配置:webpack.config.js

示例的完整构建配置如下(与 examples/code-splitting-depend-on-simple/webpack.config.js 一致):

"use strict";

/** @type {import("webpack").Configuration} */
const config = {
	entry: {
		app: { import: "./app.js", dependOn: ["react-vendors"] },
		"react-vendors": ["react", "react-dom", "prop-types"]
	},
	optimization: {
		chunkIds: "named" // To keep filename consistent between different modes (for example building only)
	},
	stats: {
		chunks: true,
		chunkRelations: true
	}
};

module.exports = config;

逐项说明:

配置项 取值 作用
entry.app.import "./app.js" 业务入口的源文件
entry.app.dependOn ["react-vendors"] 声明 app 依赖 react-vendors 入口,加载 app 前必须先加载它;app 因此不再自带运行时
entry["react-vendors"] ["react", "react-dom", "prop-types"] vendor 入口,可以是字符串数组形式(多模块入口)
optimization.chunkIds "named" 保持不同模式(dev/prod、单独构建)下 chunk 文件名一致,方便调试与对比
stats.chunks / stats.chunkRelations true 在构建输出中打印 chunk 及 chunk 之间的关系(<{react-vendors}>>{app}<),便于验证 dependOn 是否生效

官方 schema 对 dependOn 的定义(见 schemas/WebpackOptions.json)是:

"The entrypoints that the current entrypoint depend on. They must be loaded when this entrypoint is loaded." (当前入口所依赖的入口,加载当前入口时必须同时加载它们。)

取值为字符串数组,每个元素是已声明的其他入口名,minLength: 1

入口源码:app.js

examples/code-splitting-depend-on-simple/app.js 非常简洁:

import react from "react";
import reactDOM from "react-dom";
import propTypes from "prop-types";

console.log(react, reactDOM, propTypes);

注意:app.js 里直接 import 了三个第三方库,但配置上这些库由 react-vendors 入口负责打包。得益于 dependOnapp 在运行时直接复用 react-vendors 模块缓存中的实例,而不是各自打包一份。

构建产物分析

构建后生成两个文件:react-vendors.js(vendor 模块 + 完整运行时)与 app.js(仅业务代码,通过 self["webpackChunk"] 注入机制挂载到同一运行时上)。

dist/app.js

app.js 产物中只有 ./app.js 这一个模块,对 reactreact-domprop-types 的引用全部是 __webpack_require__(0/1/2) 形式的跨 chunk 请求,模块体在 react-vendors.js 中:

"use strict";
(self["webpackChunk"] = self["webpackChunk"] || []).push([["app"],{

/***/ 3
/*!****************!*\
  !*** ./app.js ***!
  \****************/
/*! namespace exports */
/*! exports [not provided] [no usage info] */
/*! runtime requirements: __webpack_require__, __webpack_require__.n, __webpack_require__.r, __webpack_exports__, __webpack_require__.* */
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ 0);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-dom */ 1);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! prop-types */ 2);
/* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(prop_types__WEBPACK_IMPORTED_MODULE_2__);

console.log((react__WEBPACK_IMPORTED_MODULE_0___default()), (react_dom__WEBPACK_IMPORTED_MODULE_1___default()), (prop_types__WEBPACK_IMPORTED_MODULE_2___default()));

/***/ }

},
/******/ __webpack_require__ => { // webpackRuntimeModules
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(moduleId))
/******/ var __webpack_exports__ = (__webpack_exec__(3));
/******/ }
]);

关键点:

  • (self["webpackChunk"] = self["webpackChunk"] || []).push([["app"], {...}]) 是 JSONP chunk 注入——app chunk 依赖 react-vendors 中已建立的 webpackChunk 全局数组,把自身模块合并进同一个模块表;
  • 文件里没有任何 __webpack_modules__ 缓存声明,说明运行时不在 app.js 中,印证了"被 dependOn 的入口携带运行时、依赖它的入口只做增量注入"的行为;
  • 末尾的 webpackRuntimeModules 只是执行入口模块 3./app.js),运行时函数 __webpack_require__react-vendors.js 提供。

dist/react-vendors.js

react-vendors.js 是真正的 bootstrap 文件,包含三个 vendor 模块、完整运行时和启动代码。模块部分与启动代码如下(完整的运行时折叠代码段可在示例生成的 README.md 中查看):

/******/ (() => { // webpackBootstrap
/******/ 	var __webpack_modules__ = ([
/* 0 */
/*!*******************************!*\
  !*** ./node_modules/react.js ***!
  \*******************************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: module */
/*! CommonJS bailout: module.exports is used directly at 1:0-14 */
/***/ ((module) => {

module.exports = 'react';

/***/ }),
/* 1 */
/***/ ((module) => {

module.exports = 'react-dom';

/***/ }),
/* 2 */
/***/ ((module) => {

module.exports = 'prop-types';

/***/ })
/******/ 	]);

启动段:

/******/
/******/ 	// startup
/******/ 	// Load entry module and return exports
/******/ 	// This entry module is referenced by other modules so it can't be inlined
/******/ 	__webpack_require__(0);
/******/ 	__webpack_require__(1);
/******/ 	let __webpack_exports__ = __webpack_require__(2);
/******/ 	__webpack_exports__ = __webpack_require__.O(__webpack_exports__);
/******/
/******/ })()
;

运行时的 jsonp chunk loading 部分会初始化 installedChunks = { "react-vendors": 0 } 并建立 self["webpackChunk"] 数组,这正是后续 app.js 能推入模块的前提。从产物结构看:react-vendors 是"运行时宿主 + vendor 模块宿主",app 是"增量 chunk"。页面中只需按顺序引入两个 <script>(先 react-vendors.js,后 app.js)即可。

stats 输出解读(Unoptimized 与 Production)

配置里 stats.chunksstats.chunkRelations 打开后,两种模式下的完整构建输出如下。

Unoptimized 模式

asset react-vendors.js 7.4 KiB [emitted] (name: react-vendors)
asset app.js 1.59 KiB [emitted] (name: app)
chunk (runtime: react-vendors) app.js (app) 139 bytes <{react-vendors}> [initial] [rendered]
  > ./app.js app
  ./app.js 139 bytes [built] [code generated]
    [no exports]
    [used exports unknown]
    entry ./app.js app
chunk (runtime: react-vendors) react-vendors.js (react-vendors) 87 bytes (javascript) 3.25 KiB (runtime) >{app}< [entry] [rendered]
  > prop-types react-vendors
  > react react-vendors
  > react-dom react-vendors
  runtime modules 3.25 KiB 6 modules
  cacheable modules 87 bytes
    ./node_modules/prop-types.js 31 bytes [built] [code generated]
      [used exports unknown]
      from origin ./app.js
        harmony side effect evaluation prop-types ./app.js 3:0-35
        harmony import specifier prop-types ./app.js 5:29-38
      cjs self exports reference ./node_modules/prop-types.js 1:0-14
      entry prop-types react-vendors
    ./node_modules/react-dom.js 30 bytes [built] [code generated]
      [used exports unknown]
      from origin ./app.js
        harmony side effect evaluation react-dom ./app.js 2:0-33
        harmony import specifier react-dom ./app.js 5:19-27
      cjs self exports reference ./node_modules/react-dom.js 1:0-14
      entry react-dom react-vendors
    ./node_modules/react.js 26 bytes [built] [code generated]
      [used exports unknown]
      from origin ./app.js
        harmony side effect evaluation react ./app.js 1:0-26
        harmony import specifier react ./app.js 5:12-17
      cjs self exports reference ./node_modules/react.js 1:0-14
      entry react react-vendors
webpack X.X.X compiled successfully

Production 模式

asset react-vendors.js 1.2 KiB [emitted] [minimized] (name: react-vendors)
asset app.js 180 bytes [emitted] [minimized] (name: app)
chunk (runtime: react-vendors) app.js (app) 139 bytes <{react-vendors}> [initial] [rendered]
  > ./app.js app
  ./app.js 139 bytes [built] [code generated]
    [no exports]
    [no exports used]
    entry ./app.js app
chunk (runtime: react-vendors) react-vendors.js (react-vendors) 87 bytes (javascript) 3.05 KiB (runtime) >{app}< [entry] [rendered]
  > prop-types react-vendors
  > react react-vendors
  > react-dom react-vendors
  runtime modules 3.05 KiB 5 modules
  cacheable modules 87 bytes
    ./node_modules/prop-types.js 31 bytes [built] [code generated]
      [used exports unknown]
      from origin ./app.js
        harmony side effect evaluation prop-types ./app.js 3:0-35
        harmony import specifier prop-types ./app.js 5:29-38
      cjs self exports reference ./node_modules/prop-types.js 1:0-14
      entry prop-types react-vendors
    ./node_modules/react-dom.js 30 bytes [built] [code generated]
      [used exports unknown]
      from origin ./app.js
        harmony side effect evaluation react-dom ./app.js 2:0-33
        harmony import specifier react-dom ./app.js 5:19-27
      cjs self exports reference ./node_modules/react-dom.js 1:0-14
      entry react-dom react-vendors
    ./node_modules/react.js 26 bytes [built] [code generated]
      [used exports unknown]
      from origin ./app.js
        harmony side effect evaluation react ./app.js 1:0-26
        harmony import specifier react ./app.js 5:12-17
      cjs self exports reference ./node_modules/react.js 1:0-14
      entry react react-vendors
webpack X.X.X compiled successfully

几个值得注意的细节:

  • chunk (runtime: react-vendors) app.js (app) ... <{react-vendors}><{react-vendors}> 表示 app 在加载关系上位于 react-vendors 之后(前向依赖),且整个编译的运行时挂在 react-vendors chunk 上;
  • >{app}<react-vendors 有子关系指向 app,即它是被 app 依赖的一方;
  • from origin ./app.jsharmony import specifier ... ./app.js 5:12-17:虽然模块归属在 react-vendors 入口,但使用来源是 ./app.js,说明依赖收集链路是完整的,只是打包归属被 dependOn 关系重定向到了 vendor 入口;
  • Production 模式 runtime modules 3.05 KiB 5 modules(Unoptimized 为 3.25 KiB 6 modules),压缩与 tree-shaking 后运行时会略减,两个模式都成功编译。

源码实现链路:dependOn 是如何生效的

从源码结构看,dependOn 从配置到产物的传递经过以下环节:

  1. 配置归一化EntryOptionPlugin.jsentryDescriptionToOptions 会把入口描述对象中的 desc.dependOn 原样带入 EntryOptionsdependOn 与其他入口选项(filenameruntimepublicPath 等)一起成为入口描述的一部分。

  2. 入口间建立依赖边Compilation.js 在处理入口组时,对被依赖的入口执行 entry.addDependOn(dependency),并通过 dependency.addChild(entry) / entry.addParent(dependency) 建立 parent/child 关系——这正是 stats 中 >{app}<<{react-vendors}> 关系的来源。

  3. Entrypoint 的数据结构Entrypoint.jsSortableSet 保存 _dependOn 集合,并提供 addDependOn(entrypoint)dependOn(entrypoint) 两个方法供 chunk 图遍历查询。

  4. 运行时归属决策ChunkGraph.js 在遍历 chunk 的入口组时,会沿 dependOn 边向下 BFS(注释中说明用 entrypoints 集合兼作 visited 集合,避免钻石形 dependOn 依赖被重复遍历),并保证"被别人依赖的入口必须先行加载"。这就是为什么运行时最终落在 react-vendors chunk 中、app chunk 只保留 JSONP 注入代码。

  5. HTML 场景的延伸HtmlModulesPlugin.jscollectHtmlEntryRequests 会按 dependOn 祖先优先的顺序收集一个 HTML 页面需要加载的资源("its dependOn ancestors first (transitive, deduped — so a diamond loads each once), then the entry's own imports"),保证输出 HTML 时共享/运行时 chunk 的 <script> 标签先于入口自身的标签注入。

实践建议与适用边界

  • 适用场景:多入口项目希望共享 vendor 与运行时(典型如"页面 A + 页面 B 都依赖同一份 React")、或模块联邦/HTML 输出中需要控制脚本加载顺序。dependOn 表达的是加载顺序与运行时归属,而不是简单的代码复用。
  • optimization.splitChunks 的分工splitChunks 面向"同一入口/单入口下按规则自动拆 chunk";dependOn 面向"多个入口之间的显式依赖关系"。多入口共享 vendor 的经典做法正是本示例这种显式声明,而不是依赖 splitChunks 自动推导。
  • 注意顺序dependOn 形成有向关系,A dependOn B 则 B 必须先行加载;若出现环形依赖,入口关系无法形成合法的加载顺序,配置时应避免。
  • 验证方式:沿用本示例的 stats: { chunks: true, chunkRelations: true },通过 <{vendor}> / >{app}< 标记确认关系建立;通过检查产物中哪个文件携带 __webpack_modules__installedChunks 确认运行时归属。
  • 文件名一致性:示例中显式设置 optimization.chunkIds: "named" 并注释了原因——"To keep filename consistent between different modes (for example building only)",在 dev/prod 切换或部分构建时避免文件名漂移导致引用失效。

小结

dependOn 是 webpack 入口选项中最能体现"入口之间拓扑关系"的机制:一行 dependOn: ["react-vendors"] 就能让 app 入口放弃自带运行时与 vendor 模块,转而挂载到 react-vendors 提供的 JSONP 运行时上。结合 examples/code-splitting-depend-on-simple/README.md 中的完整产物与 stats 输出对照阅读,再参考上文 EntryOptionPlugin.jsEntrypoint.jsChunkGraph.js 的实现链路,可以完整理解这一机制从配置解析到产物形态的全过程,并将其迁移到自己项目的多入口分包方案中。

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