webpack DllPlugin 分库预编译实战:解析 examples/dll 参考包(DllReference)的完整构建链路
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,它交代了两件事:
- 本示例是官方
DllPlugin文档中 reference(参考/被引用)bundle 的角色,负责产出 bundle 与 manifest; - 它的配套消费方是 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.name做compilation.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";
/***/ })
/******/ ]);
这份产物值得注意的机制有四点:
- module 0 是"DLL 入口包装模块",注释
!*** dll alpha ***!标明身份,它做的事只有一行:module.exports = __webpack_require__;—— 即把整个模块加载器__webpack_require__作为该 DLL 的导出。这样用户包只要拿到这个外部库,就能通过它按 module id 加载 DLL 内任意模块。 - module 1~3 是真实业务模块:
alpha.js、a.js与从node_modules解析到的module.js,各自被编译为独立闭包。由于这些模块直接使用module.exports,注释里还标注了CommonJS bailout: module.exports is used directly,说明它们无法享受 ESM 静态分析,被标记为unknown exports (runtime-defined)。 - 运行时启动(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}}}}
把它格式化后就是三个字段:
name:alpha_6a3e2c513c0f9a6ca058,对应DllPlugin的name选项(已做[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 KiB、MyDll.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 entry 与 used 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.js 的 apply:
- 在
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.js 在 make 钩子中调用 compilation.addEntry,但放入的是一个 DllEntryDependency:它把数组里的每个字符串都转成带 loc(name + index)的 EntryDependency,并注册 DllModuleFactory 来处理这种特殊依赖类型。正是这个工厂最终生成产物里的"包装模块"(module 0)。
6.3 LibManifestPlugin:emit 阶段逐 chunk 写 manifest
lib/dll/LibManifestPlugin.js 在 emit 钩子(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可以是对象或文件路径字符串。若是字符串,则在beforeCompile用inputFileSystem.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"),以及对第三方module的require("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,再引入用户包 bundle(examples/dll-user 目录中提供了对应的 example.html 与 example.js 便于本地联调)。同时由于全局名内含 [fullhash],一旦 DLL 源码变更触发 hash 变化,用户包也必须基于新 manifest 重新构建才能对上号——这也是"分库编译"天然要承担的版本同步成本。
八、应用场景与边界(基于本仓库的事实梳理)
- 适用场景:把变动频率低、体积较大的公共模块(UI 库、工具集、框架运行时)预先编译成若干 DLL,让业务代码在开发迭代时跳过这些模块的重复编译,从而获得更快的冷启动构建;多应用/多页面共享同一份 vendor 时,只需"编译一次、多处引用"。
- 实现代价:需要同时维护 reference(
DllPlugin)与 user(DllReferencePlugin)两套构建配置,并保证 manifest 的name、type、模块标识在两次构建间稳定一致;哈希类全局名会带来"DLL 一改,用户包必须重编"的连锁反应。 - 边界提示:本示例展示的是经典的"全局变量 + external"形态。
output.library使用全局变量的形式意味着它不是模块化加载方案,而是面向浏览器<script>顺序加载的设计;这一点从产物var alpha_xxx与用户包external "alpha_xxx"的对应关系可以直接验证。 - 如何复现:仓库 examples 的文档内容由模板(template.md)配合生成脚本自动产出,本地可在仓库根目录安装依赖后执行 examples 相关构建脚本(入口见 examples/buildAll.js 与 examples/examples.js,具体命令以仓库说明为准),即可在
examples/dll/dist与examples/dll-user/dist下复现本文引用的全部产物与统计。
小结
本文以 examples/dll/README.md 为主线,完整还原了 webpack DllPlugin 分库方案的"参考包"一侧:从多入口数组配置、output.library 全局命名、manifest 结构,到 DllPlugin → DllEntryPlugin → LibManifestPlugin 的源码级实现,并与 examples/dll-user 的 DllReferencePlugin 用法相互印证。掌握了 manifest 中 name/content/type 的含义以及"delegated module → dll-reference external → 全局 require"的调用链之后,你既能照着示例搭建自己的分库方案,也能在遇到"external 未定义""模块找不到"等问题时,快速定位到是 DLL 与用户包之间的哪个契约环节失配了。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00