首页
/ webpack DllPlugin 分库预编译实战:解析 examples/dll 参考包(DllReference)的完整构建链路

webpack DllPlugin 分库预编译实战:解析 examples/dll 参考包(DllReference)的完整构建链路

2026-09-07 19:58:50作者:侯霆垣

examples/dll 是 webpack 官方示例中"动态链接库(DLL)"的**参考包(Reference Bundle)**示例:它先用 DllPlugin 把多组入口及公共模块分别打包成带独立全局命名空间的 bundle,并同步产出 JSON 清单(manifest);随后由配对示例 examples/dll-user 中的 DllReferencePlugin 消费这些清单,将"业务包不重复编译第三方/公共代码"的分库思想落地。读完本文,你将掌握 DllPlugin 的完整配置、产物结构与 manifest 生成原理,并能依据源码把"编译一次、多方引用"的 DLL 工作流跑通。

本文所有配置、产物与统计均来自当前仓库真实内容,涉及路径均可直接打开核对:examples/dll/README.md(关联文档)、examples/dll/webpack.config.js(配置)以及 lib/dll 目录下插件源码。

一、示例整体定位:一个"参考包",等待"用户包"引用

先看该示例的生成模板 examples/dll/template.md,它交代了两件事:

  1. 本示例是官方 DllPlugin 文档中 reference(参考/被引用)bundle 的角色,负责产出 bundle 与 manifest;
  2. 它的配套消费方是 examples/dll-user,即 _user(使用者)bundle。

examples/dll/template.md 中可以看到,README.md 的主体其实是模板占位符被真实构建结果替换后的产物:

_{{webpack.config.js}}_        → 替换为配置文件真实内容
_{{dist/MyDll.alpha.js}}_      → 替换为打包产物
_{{dist/alpha-manifest.json}}_ → 替换为清单文件
_{{stdout}}_ / {{production:stdout}}_ → 替换为 dev/production 构建统计

也就是说,关联文档里展示的 bundle 代码、manifest 与 Info 统计,都是仓库示例生成流程用真实运行输出渲染出来的,具有相当的可信度。因此本文后续讲解可以放心把文档中的输出当作可验证事实。

1.1 入口依赖模块一览

该示例的源码模块都非常精简,便于观察映射关系:

文件 内容 角色
examples/dll/alpha.js module.exports = "alpha" alpha 入口之一
examples/dll/a.js module.exports = "a" alpha 入口之一
examples/dll/beta.js module.exports = "beta" beta 入口之一
examples/dll/b.js module.exports = "b" beta 入口之一
examples/dll/c.jsx module.exports = "jsx" beta 入口之一(演示 .jsx 扩展名解析)

注意 c.jsx 之所以能被 ./c 命中,靠的是配置里的 resolve.extensions: [".js", ".jsx"]

二、webpack.config.js 逐行精读

文档中的核心配置与仓库内 examples/dll/webpack.config.js 完全一致:

"use strict";

const path = require("path");
const webpack = require("../../");   // 引入仓库自身的 webpack 实现

/** @type {import("webpack").Configuration} */
const config = {
	// mode: "development" || "production",
	resolve: {
		extensions: [".js", ".jsx"]
	},
	entry: {
		alpha: ["./alpha", "./a", "module"],
		beta: ["./beta", "./b", "./c"]
	},
	output: {
		path: path.join(__dirname, "dist"),
		filename: "MyDll.[name].js",
		library: "[name]_[fullhash]"
	},
	plugins: [
		new webpack.DllPlugin({
			path: path.join(__dirname, "dist", "[name]-manifest.json"),
			name: "[name]_[fullhash]"
		})
	]
};

module.exports = config;

逐项说明这些配置在 DLL 场景中的含义:

  • entry 是一个"入口数组":与普通入口不同,这里每组 entry 会同时被当作 DLL 的多个并列入口模块。alpha: ["./alpha", "./a", "module"] 意味着 alpha 这一个 DLL chunk 中同时包含 ./alpha./a 以及名为 module 的模块(后者被解析到 ../node_modules/module.js,用于演示第三方依赖同样可以打进 DLL)。
  • output.library: "[name]_[fullhash]":决定 DLL bundle 暴露的全局变量名[name] 取 entry 名(alpha/beta),[fullhash] 是该次构建的全量哈希。最终生成的全局名形如 alpha_6a3e2c513c0f9a6ca058,运行时以 var alpha_xxx = ... 的形式挂到全局,供用户包以 external 方式引用。
  • new webpack.DllPlugin({ path, name }) 的两个选项都需要能够被 [name]/[fullhash] 等占位符替换:
    • path:manifest JSON 的输出绝对路径,这里每个 entry 各写一个 dist/[name]-manifest.json
    • name:manifest 中记录的"外部库名",必须与 output.library 保持一致(源码层面由 LibManifestPlugin 对 options.namecompilation.getPath 替换,详见下文)。
  • require("../../") 说明这些 examples 直接使用仓库自身的 webpack 来构建自己,方便复现与调试。

三、构建产物剖析:MyDll.alpha.js 里到底装了什么

文档展示的 [dist/MyDll.alpha.js](产物,构建后生成于 examples/dll/dist 下)内部结构如下(节选核心部分,保留文档原有语义):

var alpha_6a3e2c513c0f9a6ca058;
/******/ (() => { // webpackBootstrap
/******/ 	var __webpack_modules__ = ([
/* 0 */
/*!*****************!*\
  !*** dll alpha ***!
  \*****************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
module.exports = __webpack_require__;
/***/ }),
/* 1 */
/*!******************!*\
  !*** ./alpha.js ***!
  \******************/
/***/ ((module) => {
module.exports = "alpha";
/***/ }),
/* 2 */
/*!******************!*\
  !*** ./a.js ***!
  \******************/
/***/ ((module) => {
module.exports = "a";
/***/ }),
/* 3 */
/*!*******************************!*\
  !*** ../node_modules/module.js ***!
  \*******************************/
/***/ ((module) => {
module.exports = "module";
/***/ })
/******/ 	]);

这份产物值得注意的机制有四点:

  1. module 0 是"DLL 入口包装模块",注释 !*** dll alpha ***! 标明身份,它做的事只有一行:module.exports = __webpack_require__; —— 即把整个模块加载器 __webpack_require__ 作为该 DLL 的导出。这样用户包只要拿到这个外部库,就能通过它按 module id 加载 DLL 内任意模块。
  2. module 1~3 是真实业务模块alpha.jsa.js 与从 node_modules 解析到的 module.js,各自被编译为独立闭包。由于这些模块直接使用 module.exports,注释里还标注了 CommonJS bailout: module.exports is used directly,说明它们无法享受 ESM 静态分析,被标记为 unknown exports (runtime-defined)
  3. 运行时启动(startup)部分(文档中位于折叠的 runtime 代码之后)加载入口模块并把导出赋给全局变量:
// startup
// Load entry module and return exports
let __webpack_exports__ = __webpack_require__(0);
alpha_6a3e2c513c0f9a6ca058 = __webpack_exports__;

即执行 __webpack_require__(0) 拿到 __webpack_require__ 本身,再赋给全局 alpha_6a3e2c513c0f9a6ca058。注释还特别说明:入口模块没有可内联的顶层声明,因此它不能被 scope hoisting 内联。 4. webpack 运行时(runtime) 是本仓库标准 IIFE 形态:模块缓存对象 __webpack_module_cache__ + __webpack_require__(moduleId),执行模块函数后返回 module.exports。这保证了 DLL 包内部拥有完整的模块加载能力。

从产物可以推断:manifest 的 name(如 alpha_6a3e2c513c0f9a6ca058)必须与产物暴露的全局变量名严格一致,这是用户包能以 external 方式取到 __webpack_require__ 的前提。

四、Manifest 清单结构精读

文档给出 alpha 的完整清单(构建后生成于 examples/dll/dist/alpha-manifest.json):

{"name":"alpha_6a3e2c513c0f9a6ca058","content":{"./alpha.js":{"id":1,"buildMeta":{"treatAsCommonJs":true}},"./a.js":{"id":2,"buildMeta":{"treatAsCommonJs":true}},"../node_modules/module.js":{"id":3,"buildMeta":{"treatAsCommonJs":true}}}}

把它格式化后就是三个字段:

  • namealpha_6a3e2c513c0f9a6ca058,对应 DllPluginname 选项(已做 [name]_[fullhash] 替换),也即 DLL 产物暴露的全局变量名。
  • type:该示例未配置 output.libraryTarget,故 manifest 中没有 type 字段;若设置 libraryTarget,则此处会记录(参见 lib/dll/LibManifestPlugin.js 的 typedef 注释中 type:external type,取值 output.libraryTarget)。
  • content:把模块请求标识(module libIdent)映射到 DLL 内部的 module id。例如用户包中的 ../dll/alpha 会被解析成标识 ./alpha.js,对照清单得到 id: 1,运行时即 dllRequire(1)

content 的 key 为什么是 ./alpha.js./a.js../node_modules/module.js 这类相对请求,而不是数字?因为 libIdent(库内唯一标识)在"dll 包"与"用户包"两次独立构建之间是稳定的,而数字 module id 可能因模块顺序变化而漂移。manifest 在这里充当"两次构建之间的稳定契约"。

需要特别指出:content只有 3 个真实模块,并没有 module 0(dll alpha 包装模块)。这与 DllPlugin 默认 entryOnly: true 有关——LibManifestPlugin 在收集模块时会过滤掉"没有 EntryDependency 入边连接"的模块(对应源码 lib/dll/LibManifestPlugin.js 中的判断逻辑),而包装模块由 DllEntryDependency 生成、不作为普通条目进入 content。这也解释了为什么要由 module 0 暴露 __webpack_require__ 给外部:用户包拿到 require 函数后自行按 id 调用即可,无需在 manifest 里暴露包装模块本身。

五、构建统计对比:Unoptimized vs Production

文档 Info 部分给出了该示例两种模式的真实构建统计,这里整理为表格:

维度 Unoptimized(开发默认) Production mode
产物大小 MyDll.alpha.js 2.58 KiBMyDll.beta.js 2.55 KiB MyDll.alpha.js 321 bytes [minimized]MyDll.beta.js 315 bytes [minimized]
alpha chunk 85 bytes,含 3 个 dependent modules 共 73 bytes;dll alpha 12 bytes 相同结构,仅产物被压缩
beta chunk 81 bytes,含 3 个 dependent modules 共 69 bytes 相同结构
模块标记 [used exports unknown] / used as library export / dll entry [used exports unknown]
结果 webpack X.X.X compiled successfully 同左

(文档中的 webpack X.X.X 是示例生成流程对版本号的统一占位符,两种模式的原始输出分别记录于文档 examples/dll/README.md 的 Info 小节。)

统计中值得关注的细节:模块被标记为 dll entryused as library export,说明这些模块是作为库导出参与构建的,且由于 DLL 对外只暴露 require 函数,每个模块的导出使用情况是"未知"的([used exports unknown]),因此 entryOnly 关闭场景下必须配合"把所有模块标记为已使用"来防止 tree-shaking 误删(见下文源码分析)。开发与生产产物体积差距(2.6 KiB → 约 320 B)主要由 minification 贡献。

六、深入源码:DllPlugin 家族如何协同工作

示例只是冰山一角,真正支撑它的是 lib/dll 目录下的插件族。结合本示例的运行路径,整条调用链可以还原为以下四步:

6.1 DllPlugin.apply:把每个入口拆给 DllEntryPlugin,再挂 LibManifestPlugin

lib/dll/DllPlugin.jsapply

  • validate 钩子用 schemas/plugins/dll/DllPlugin.json 校验参数;
  • entryOption 钩子中遍历 Object.keys(entry),为每个入口实例化一个 DllEntryPlugin(context, entry[name].import, { name })(并明确指出暂不支持 function 形式的动态入口);
  • 随后 new LibManifestPlugin({ ...this.options, entryOnly }) 挂到同一 compiler;
  • entryOnly 默认取 options.entryOnly !== false 即默认为 true。当显式设为 false 时,还会额外挂载 FlagAllModulesAsUsedPlugin,并把 normalModuleFactory 产出的每个模块标记为 sideEffectFree = false——这正是为了保证关闭 entryOnly 后 DLL 里的依赖模块不会被当作无副作用代码而摇掉。

6.2 DllEntryPlugin:把数组入口包装成 DllEntryDependency

lib/dll/DllEntryPlugin.jsmake 钩子中调用 compilation.addEntry,但放入的是一个 DllEntryDependency:它把数组里的每个字符串都转成带 loc(name + index)的 EntryDependency,并注册 DllModuleFactory 来处理这种特殊依赖类型。正是这个工厂最终生成产物里的"包装模块"(module 0)。

6.3 LibManifestPlugin:emit 阶段逐 chunk 写 manifest

lib/dll/LibManifestPlugin.jsemit 钩子(stage 110)对每个 canBeInitial() 的 chunk 执行:

  • compilation.getPath(this.options.path, { chunk }) 计算 manifest 实际输出路径(对应配置里 [name]-manifest.json[name] 替换),并用 Set 检测"每个 chunk 必须拥有唯一路径",重复则报错(对应 issue #18200);
  • compilation.getPath(this.options.name, { chunk, contentHashType: "javascript" }) 计算 manifest 的 name[fullhash] 在这里被替换);
  • 遍历该 chunk 的模块(按 module id 排序以保证确定性),过滤规则即上文提到的 entryOnly 判断:entryOnly && 模块没有 EntryDependency 入边 → 跳过
  • 对保留下来的模块记录 { id, buildMeta, exports },key 采用 module.libIdent({ context: this.options.context || compiler.context })
  • 最终序列化为 { name, type, content }format 选项为 true 时用 JSON.stringify(manifest, null, 2) 美化,否则压缩成一行(文档中的 manifest 即紧凑格式),然后 mkdirp + writeFile 落到 path

6.4 DllReferencePlugin:用户包一侧的"消费"逻辑

lib/dll/DllReferencePlugin.js 与本文示例互补,它说明了参考包产物"被谁、以什么方式使用":

  • manifest 可以是对象或文件路径字符串。若是字符串,则在 beforeCompileinputFileSystem.readFile 读取并用 parseJson 解析;解析失败不会中断进程,而是把 DllManifestError(消息形如 Dll manifest <path> ...)暂存,在后续 compilation 钩子中 push 到 compilation.errors 使构建失败,同时把 manifest 路径登记为 fileDependencies(参与增量监听);
  • 若 options 未显式给 name/sourceType/content,会依次回退到 manifest 中的 name/type/content(源码 if (!name) name = manifest.name 等逻辑);
  • 插件构造 external 键 dll-reference <name> → 值 <name>,通过 ExternalModuleFactoryPlugin(sourceType || "var", externals) 注册,于是用户包解析到这类模块时,生成的代码就是 module.exports = alpha_6a3e2c513c0f9a6ca058;(全局变量直取,对应文档产物中 external "alpha_..." 模块);
  • 再把 DelegatedModuleFactoryPlugin 挂到 normalModuleFactory,把业务请求映射为"委托模块"。

这些逻辑都能在 examples/dll-user/README.md 展示的产物里找到实证:例如用户包中 require("../dll/alpha") 被编译成

module.exports = (__webpack_require__(/*! dll-reference alpha_6a3e2c513c0f9a6ca058 */ 2))(1);

即:先从 external 模块(id 2,内容 module.exports = alpha_6a3e2c513c0f9a6ca058)取得 DLL 的 __webpack_require__,再用 manifest 中的 id 1(对应 ./alpha.js)去取模块。整条"用户包 → delegated module → dll-reference external → 全局 DLL require → 模块内容"的链路由此闭合。

七、与 dll-user 配对阅读:两种引用姿势

真正能"动起来"的用法需要与配套示例 examples/dll-user 放在一起看。其配置 examples/dll-user/webpack.config.js 挂了两个 DllReferencePlugin

new webpack.DllReferencePlugin({
	context: path.join(__dirname, "..", "dll"),
	manifest: require("../dll/dist/alpha-manifest.json")
}),
new webpack.DllReferencePlugin({
	scope: "beta",
	manifest: require("../dll/dist/beta-manifest.json"),
	extensions: [".js", ".jsx"]
})
  • 第一个插件不设 scope:manifest 里的模块标识(如 ./alpha.js)会直接匹配用户包中按相对路径解析出来的请求,所以 examples/dll-user/example.js 里可以写 require("../dll/alpha")require("../dll/a"),以及对第三方 modulerequire("module")(它同样进了 alpha 的 manifest)。
  • 第二个插件设置了 scope: "beta":manifest 的模块会被装进 beta/ 命名空间,于是用户包只能通过 require("beta/beta")require("beta/b")require("beta/c") 访问。scope 相当于给 DLL 内容加了一层"命名域",避免多份 DLL 之间的标识冲突。
  • extensions: [".js", ".jsx"] 用于在用户包解析 .jsx 请求时也能命中 manifest 中的 ./c.jsx
  • 反过来看,这正是 examples/dll 文档里"reference bundle(with the manifests)"这一角色的完整含义:先构建 reference 包得到 bundle + manifest,再让 user 包只读 manifest 做编译期映射、运行期依赖全局变量

另外可以推断运行时约束:用户包产物中对外部库的引用是裸的全局变量(module.exports = alpha_xxx),因此 HTML 页面必须先以 <script> 引入 DLL bundle,再引入用户包 bundleexamples/dll-user 目录中提供了对应的 example.html 与 example.js 便于本地联调)。同时由于全局名内含 [fullhash],一旦 DLL 源码变更触发 hash 变化,用户包也必须基于新 manifest 重新构建才能对上号——这也是"分库编译"天然要承担的版本同步成本。

八、应用场景与边界(基于本仓库的事实梳理)

  • 适用场景:把变动频率低、体积较大的公共模块(UI 库、工具集、框架运行时)预先编译成若干 DLL,让业务代码在开发迭代时跳过这些模块的重复编译,从而获得更快的冷启动构建;多应用/多页面共享同一份 vendor 时,只需"编译一次、多处引用"。
  • 实现代价:需要同时维护 reference(DllPlugin)与 user(DllReferencePlugin)两套构建配置,并保证 manifest 的 nametype、模块标识在两次构建间稳定一致;哈希类全局名会带来"DLL 一改,用户包必须重编"的连锁反应。
  • 边界提示:本示例展示的是经典的"全局变量 + external"形态。output.library 使用全局变量的形式意味着它不是模块化加载方案,而是面向浏览器 <script> 顺序加载的设计;这一点从产物 var alpha_xxx 与用户包 external "alpha_xxx" 的对应关系可以直接验证。
  • 如何复现:仓库 examples 的文档内容由模板(template.md)配合生成脚本自动产出,本地可在仓库根目录安装依赖后执行 examples 相关构建脚本(入口见 examples/buildAll.jsexamples/examples.js,具体命令以仓库说明为准),即可在 examples/dll/distexamples/dll-user/dist 下复现本文引用的全部产物与统计。

小结

本文以 examples/dll/README.md 为主线,完整还原了 webpack DllPlugin 分库方案的"参考包"一侧:从多入口数组配置、output.library 全局命名、manifest 结构,到 DllPlugin → DllEntryPlugin → LibManifestPlugin 的源码级实现,并与 examples/dll-userDllReferencePlugin 用法相互印证。掌握了 manifest 中 name/content/type 的含义以及"delegated module → dll-reference external → 全局 require"的调用链之后,你既能照着示例搭建自己的分库方案,也能在遇到"external 未定义""模块找不到"等问题时,快速定位到是 DLL 与用户包之间的哪个契约环节失配了。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390