Webpack DllUser 示例全解析:基于 DllReferencePlugin 与 manifest 清单构建 DLL 用户包
本技术指南以 webpack 仓库中的官方示例 examples/dll-user(DLL 用户侧)为核心,结合其配套的 examples/dll(DLL 生产侧)示例与 lib/dll/ 源码,完整讲解“先由 DllPlugin 把第三方/公共模块预编译成独立 DLL bundle,再由 DllReferencePlugin 通过 manifest 清单把这些模块以『委派模块(Delegated Module)』的形式链接回业务包”的两段式构建方案。读完本文,你将掌握 DllReferencePlugin 的 context、manifest、scope、extensions 四个核心配置的含义与组合方式,理解 manifest 的 JSON 结构与运行时 external 全局变量的对应关系,并能据此在自己项目中复现“分包预编译 + 主包引用”的构建组织。
一、先看全景:DLL 的“生产端”与“用户端”如何配对
examples/dll-user 这个示例属于 webpack 官方 examples 套件中成对出现的两个目录之一,两个示例互为“上下游”:
- 生产端(reference bundle):examples/dll 负责把一批模块打包成一个(或多个)带全局
library变量的独立产物,并同步导出一份记录“模块路径 → 内部模块 id”映射的*-manifest.json清单; - 用户端(user bundle):examples/dll-user 就是本文的关联文档,它在业务包中通过
DllReferencePlugin读取上述 manifest,让require()/import指向 DLL 中已经编译好的模块,业务包本身不再重复打包这些模块。
这种结构带来的直接收益是:业务包构建时不需要再次解析、编译被公共化的那批模块,从而缩短构建时间;同时公共模块的产物可以独立缓存、独立发布(例如由 <script> 先行加载)。下面先从用户端的关键文件开始拆解。
二、用户端示例的目录与文件
examples/dll-user 目录下的结构如下:
README.md— 构建产物说明(本文核心文档)webpack.config.js— 使用DllReferencePlugin的配置example.js— 业务入口,展示各种对 DLL 内模块的引用方式example.html— 演示 DLL 产物与业务产物在页面中的加载顺序template.md— examples 套件生成文档所用的模板
其中真正决定“用户端”行为的只有 examples/dll-user/webpack.config.js 与 examples/dll-user/example.js 两个文件,其余产物说明由 examples 套件的构建/文档生成流程产出(README 的 Info 段中的 webpack X.X.X 占位符即来自该生成流程的真实构建统计)。
三、用户端配置逐行拆解:一次注入两个 DLL
完整配置如下(来源 examples/dll-user/webpack.config.js):
"use strict";
const path = require("path");
const webpack = require("../../");
/** @type {import("webpack").Configuration} */
const config = {
// mode: "development" || "production",
plugins: [
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"]
})
]
};
module.exports = config;
注意它在一条配置里同时挂了两个 DllReferencePlugin 实例,分别消费生产端产出的 alpha 与 beta 两份 manifest。这是示例最想演示的点:一个用户包可以同时引用多个 DLL。各配置项含义如下。
| 配置项 | alpha 实例 | beta 实例 | 作用 |
|---|---|---|---|
context |
path.join(__dirname, "..", "dll") |
未设置 | 解析 manifest 内容键(如 ./alpha.js)所基于的绝对上下文,把“模块路径”与“DLL 内模块”对上号 |
manifest |
require("../dll/dist/alpha-manifest.json") |
require("../dll/dist/beta-manifest.json") |
指向生产端构建生成的清单对象(或清单文件路径字符串) |
scope |
未设置 | "beta" |
为该 DLL 内的模块引入一层请求前缀命名空间 |
extensions |
未设置 | [".js", ".jsx"] |
请求未带扩展名时,依次追加这些扩展名去匹配 manifest 内容键 |
3.1 manifest 与 context:按路径精确“认领”模块
manifest 既可以像本示例这样直接用 require() 注入一个对象,也可以传一个 JSON 文件路径字符串。后一种情况下,lib/dll/DllReferencePlugin.js 会在 beforeCompile 阶段通过 compiler.inputFileSystem.readFile 异步读取并用 parseJson 解析——若清单文件损坏或为空,错误会被暂存并最终以 DllManifestError 形式追加为编译错误,而不是直接中断进程。
context 决定 manifest 里的内容键(例如 ./alpha.js)相对哪个目录解释。alpha 实例没有设置 scope,属于“路径直接匹配”模式:业务代码写 require("../dll/alpha"),webpack 解析到真实的模块文件后,通过模块的 libIdent 方法基于 context(即 ../dll 目录)计算标识,得到恰好与 manifest 内容键一致的 ./alpha.js,于是命中 alpha 清单。
3.2 scope 与 extensions:用命名空间避免跨 DLL 冲突
再看 beta 实例:设置了 scope: "beta",因此业务代码必须写成 require("beta/b") 这种带前缀的形式。实现上,lib/dll/DelegatedModuleFactoryPlugin.js 只在请求以 ${scope}/ 开头时接管模块生产:它把 beta/ 前缀剥掉,得到相对请求 ./b,然后按如下顺序在 manifest 的 content 里查找:
- 精确命中:
./b直接出现在 content 键中; - 追加扩展名:依次尝试
extensions数组中的每一项,例如把./b变成./b.js、把./c变成./c.jsx(这正是示例中beta/c最终映射到c.jsx的原因); - 目录解析:若都不命中,再尝试
./xxx/index+ 各扩展名(模拟默认 resolver 的目录 index 行为)。
而 extensions 的默认值在构造器中给出:["", ".js", ".json", ".wasm"](见 lib/dll/DelegatedModuleFactoryPlugin.js)。beta 实例显式覆盖为 [".js", ".jsx"],正是为了能匹配到 DLL 中那个带 .jsx 扩展名的 c.jsx——这说明当生产端打包了非常规扩展名(如 .jsx)的模块时,用户端必须把相应扩展名补进 extensions,否则无法在清单中命中。
3.3 无 scope 时走哪条路径
若像 alpha 实例那样不设 scope,DelegatedModuleFactoryPlugin 走的是另一条分支:它挂在 normalModuleFactory.hooks.module 上,对已经解析出的真实模块取其 libIdent(options)(相对 context 的标识),若该标识存在于 manifest 内容键集合中,就把它替换成委派模块。这正是 require("../dll/alpha")、require("../dll/a") 能被 alpha 清单“认领”,而普通业务模块不被误伤的原因。
四、从入口代码看请求如何被“翻译”
业务入口 examples/dll-user/example.js 共有 6 条 require,它们被拆解成两种引用风格:
console.log(require("../dll/alpha")); // alpha 清单(无 scope,路径直配)
console.log(require("../dll/a")); // alpha 清单(无 scope,路径直配)
console.log(require("beta/beta")); // beta 清单(scope 命名空间)
console.log(require("beta/b")); // beta 清单
console.log(require("beta/c")); // beta 清单,经 extensions 命中 .jsx
console.log(require("module")); // alpha 清单中的 node_modules 模块
最后一行 require("module") 值得注意:它引用的是一个 node_modules 下的模块,生产端把其打包进了 alpha DLL(manifest 中的内容键是 ../node_modules/module.js),因此用户端只需要“借用”它而无需再次打包。
将这份入口代码与用户端构建产物 output.js 的模块表对照,可以看清每条请求最终落到的产物模块:
| 业务请求 | 产物模块 id | 委派到的 DLL 内容 | 属主 DLL |
|---|---|---|---|
../dll/alpha |
模块 1 | ./alpha.js |
alpha |
../dll/a |
模块 3 | ./a.js |
alpha |
beta/beta |
模块 4 | ./beta.js |
beta |
beta/b |
模块 6 | ./b.js |
beta |
beta/c |
模块 7 | ./c.jsx |
beta |
module |
模块 8 | ../node_modules/module.js |
alpha |
而模块 2 与模块 5 则是两个“代理源”,其职责是兜住对 DLL 全局变量 alpha_6a3e2c513c0f9a6ca058、beta_6a3e2c513c0f9a6ca058 的访问,详见下一节。
五、运行时机制:DelegatedModule + dll-reference external
5.1 产物里长什么样
以 examples/dll-user/README.md 展示的 dist/output.js 为例,require("../dll/alpha") 最终生成如下委派模块:
/* 1 */
/*! delegated ./alpha.js from dll-reference alpha_6a3e2c513c0f9a6ca058 !*/
((module, __unused_webpack_exports, __webpack_require__) => {
module.exports = (__webpack_require__(
/*! dll-reference alpha_6a3e2c513c0f9a6ca058 */ 2
))(1);
});
而模块 2 是真正承接全局变量的“external 模块”:
/* 2 */
/*! external "alpha_6a3e2c513c0f9a6ca058" !*/
((module) => {
"use strict";
module.exports = alpha_6a3e2c513c0f9a6ca058;
});
5.2 两层结构的含义
这段输出把 DLL 引用的运行原理揭示得很清楚,可以拆成两层理解:
- 外层的
(require(...))(1):(1)是模块在 DLL 包内部的模块 id。委派模块先把 DLL 的模块加载函数(即 DLL bundle 内部暴露出的__webpack_require__,见 alpha DLL 中 id 为 0 的dll alpha入口模块module.exports = __webpack_require__)当作 external 拿回来,再以 id 1 调用它,最终取到 DLL 内部的./alpha.js的实现; - 全局变量名:
alpha_6a3e2c513c0f9a6ca058来自生产端output.library与DllPlugin的name模板[name]_[fullhash]。也就是说,DLL bundle 会把自身挂到全局变量alpha_<hash>上,而用户端通过 external 直接读取这个全局变量。
在源码层,上述“外部代理源”由 lib/dll/DllReferencePlugin.js 构造:它把 dll-reference <name> 声明为一个值为 <name> 的 external(ExternalModuleFactoryPlugin 以 sourceType || "var" 处理);每个委派模块内部则通过 DelegatedSourceDependency 依赖到该 external 源(见 lib/dll/DelegatedModule.js)。同时,name、sourceType、content 都可以在用户未显式给出时从 manifest 回退取值:if (!name) name = manifest.name;(lib/dll/DllReferencePlugin.js),这保证了同一份清单的“名称一致性”由文件本身承载。
5.3 为什么全局名带 hash
name 采用 [name]_[fullhash] 后,只要 DLL 内容发生变化,其内部模块结构与缓存指纹就会改变,fullhash 随之变化,全局变量名也随之更新——这是为了避免浏览器缓存旧 DLL、而新业务包仍引用旧全局名导致的错位。代价是:生产端与用户端必须共用同一套构建时的 fullhash 结果(即基于同一份源代码同时产出 DLL 与 manifest,再由 manifest 驱动用户端),这正是本示例中“alpha/beta 的 DLL 产物与 manifest 都来自 examples/dll 这一次构建”的原因。
六、回看生产端:manifest 是如何生成的
用户端消费的 alpha-manifest.json、beta-manifest.json 与 MyDll.alpha.js、MyDll.beta.js 由配对的 examples/dll/webpack.config.js 一次性产出,其核心配置如下(完整内容见 examples/dll/README.md):
const config = {
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]"
})
]
};
生产端的 DllPlugin 从源码角度由三部分协同完成(见 lib/dll/DllPlugin.js):
DllEntryPlugin:把配置中的每个 entry 名(alpha/beta)注册成一个“dll 入口”,并注入一个dll <name>的特殊入口模块,该模块module.exports = __webpack_require__(即 DLL 对外的“加载函数出口”);LibManifestPlugin:负责把“模块相对路径 → DLL 内部模块 id”的映射序列化为[name]-manifest.json;- 选项
entryOnly(默认true):只把入口模块当作公开导出;当置为false时,还会用FlagAllModulesAsUsedPlugin强制保留全部模块并关闭副作用优化。
关键对应关系如下,产出示例 manifest(见 examples/dll/README.md 的 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必须等于output.library(这里是alpha_6a3e2c513c0f9a6ca058),因为用户端会用这个名字拼出dll-reference <name>external 并读取同名全局变量;content的每个键是模块相对DllPlugin构建上下文的可解析标识(与用户端context选项对齐),每个值里的id是该模块在MyDll.alpha.js内部模块表中的编号。对照上文用户端产物(require("dll-reference alpha_..."))(1)中参数1,可以发现它正是这里的"./alpha.js": { "id": 1 }——两端通过 manifest 精确对上了 DLL 内部模块 id。
从源码还可确认一条限制:DllPlugin 的入口必须是对象形式的静态 entry,如果传入函数形式的动态入口会直接抛出 DllPlugin doesn't support dynamic entry (function) yet(lib/dll/DllPlugin.js)。
七、页面加载顺序:DLL 必须先行
DLL 全局变量由 DLL 产物自己在浏览器中创建,因此引用方产物必须在 DLL 产物之后加载。examples/dll-user/example.html 演示了这个顺序:
<script src="../dll/js/MyDll.alpha.js" charset="utf-8"></script>
<script src="../dll/js/MyDll.beta.js" charset="utf-8"></script>
<script src="js/output.js" charset="utf-8"></script>
如果把这个顺序颠倒(例如 output.js 先于 MyDll.alpha.js 执行),业务代码在访问 alpha_6a3e2c513c0f9a6ca058 全局变量时会得到 undefined,委派模块调用 DLL 内部模块 id 将直接失败。这也是所有“DLL + 业务包”落地项目共同的硬约束:先全局注入 DLL,再加载依赖它的用户包(同理也适用于后续可能的分包并发加载场景)。
八、产物差异与体积参考(Stats Info 解读)
examples/dll-user/README.md 末尾的 Info 记录了该示例在两种模式下的真实统计:
- Unoptimized:
output.js5.49 KiB(main),其中业务入口./example.js本身仅 205 bytes,另有 8 个 dependent 模块合计 336 bytes; - Production mode:
output.js压缩后仅 575 bytes,./example.js显示[no exports used]。
把这两个数字与同目录下真实的模块代码(205 字节的入口 + 委派包装)对比,可以直观体会到“模块体量几乎全部留在 DLL 中、用户包只保留薄薄一层委派层”的效果——这正是该示例要传达的核心收益:业务包变更时只需要增量重建这层引用,DLL 中占大头的第三方/公共代码无需重新编译。
九、使用要点与边界(结论自查清单)
综合示例与源码,落地“DllPlugin + DllReferencePlugin”方案时请重点核对以下几点:
- 路径键必须一致:生产端 manifest 的
content键与用户端解析出的libIdent(受用户端context影响)必须能精确对上;对无scope的 DLL,用户端context应指向能还原出./alpha.js这类键的目录(本例为../dll); scope是用户端视角的命名空间:设置了scope: "beta"后,请求必须以beta/开头,插件剥前缀后在content中匹配;同名模块如果同时存在于多个无 scope 的 DLL,可能因键冲突而无法区分,这正是示例为 beta 引入 scope 的原因;extensions决定命中率:显式列出 DLL 中所含模块的真实扩展名(如.jsx),否则无扩展名请求可能匹配失败;默认补全集合为["", ".js", ".json", ".wasm"];name三处必须联动:output.library(生产端)、DllPlugin.name(生产端 manifest 内的name)、用户端通过 manifest 回退得到的 external 名,三处是同一个值;- 加载顺序:DLL 产物必须先于用户包在页面中执行;
- 限制:
DllPlugin不支持动态 entry;用户端 manifest 读取在beforeCompile阶段异步完成,清单缺失/损坏时以编译错误呈现而非直接崩溃。
十、深入阅读:相关源码与配套示例路径
若想继续追踪本文涉及的实现细节,可以按以下路径在仓库中查看:
- 生产端与用户端配对示例:examples/dll/README.md、examples/dll/webpack.config.js、examples/dll-user/webpack.config.js
- 用户端插件实现:lib/dll/DllReferencePlugin.js
- 委派模块与“scope/extensions/content 匹配”核心逻辑:lib/dll/DelegatedModuleFactoryPlugin.js、lib/dll/DelegatedModule.js
- 生产端插件实现:lib/dll/DllPlugin.js、lib/dll/DllEntryPlugin.js、lib/dll/LibManifestPlugin.js
- 参数校验 schema(构建期由
compiler.validate自动校验):schemas/plugins/dll/DllReferencePlugin.json、schemas/plugins/dll/DllPlugin.json
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 StartedRust0627
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