首页
/ Webpack DllUser 示例全解析:基于 DllReferencePlugin 与 manifest 清单构建 DLL 用户包

Webpack DllUser 示例全解析:基于 DllReferencePlugin 与 manifest 清单构建 DLL 用户包

2026-09-07 16:02:22作者:卓艾滢Kingsley

本技术指南以 webpack 仓库中的官方示例 examples/dll-user(DLL 用户侧)为核心,结合其配套的 examples/dll(DLL 生产侧)示例与 lib/dll/ 源码,完整讲解“先由 DllPlugin 把第三方/公共模块预编译成独立 DLL bundle,再由 DllReferencePlugin 通过 manifest 清单把这些模块以『委派模块(Delegated Module)』的形式链接回业务包”的两段式构建方案。读完本文,你将掌握 DllReferencePlugin 的 contextmanifestscopeextensions 四个核心配置的含义与组合方式,理解 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.jsexamples/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 实例,分别消费生产端产出的 alphabeta 两份 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 manifestcontext:按路径精确“认领”模块

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 scopeextensions:用命名空间避免跨 DLL 冲突

再看 beta 实例:设置了 scope: "beta",因此业务代码必须写成 require("beta/b") 这种带前缀的形式。实现上,lib/dll/DelegatedModuleFactoryPlugin.js 只在请求以 ${scope}/ 开头时接管模块生产:它把 beta/ 前缀剥掉,得到相对请求 ./b,然后按如下顺序在 manifest 的 content 里查找:

  1. 精确命中:./b 直接出现在 content 键中;
  2. 追加扩展名:依次尝试 extensions 数组中的每一项,例如把 ./b 变成 ./b.js、把 ./c 变成 ./c.jsx(这正是示例中 beta/c 最终映射到 c.jsx 的原因);
  3. 目录解析:若都不命中,再尝试 ./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 实例那样不设 scopeDelegatedModuleFactoryPlugin 走的是另一条分支:它挂在 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_6a3e2c513c0f9a6ca058beta_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.libraryDllPluginname 模板 [name]_[fullhash]。也就是说,DLL bundle 会把自身挂到全局变量 alpha_<hash> 上,而用户端通过 external 直接读取这个全局变量。

在源码层,上述“外部代理源”由 lib/dll/DllReferencePlugin.js 构造:它把 dll-reference <name> 声明为一个值为 <name> 的 external(ExternalModuleFactoryPluginsourceType || "var" 处理);每个委派模块内部则通过 DelegatedSourceDependency 依赖到该 external 源(见 lib/dll/DelegatedModule.js)。同时,namesourceTypecontent 都可以在用户未显式给出时从 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.jsonbeta-manifest.jsonMyDll.alpha.jsMyDll.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.mddist/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) yetlib/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 记录了该示例在两种模式下的真实统计:

  • Unoptimizedoutput.js 5.49 KiB(main),其中业务入口 ./example.js 本身仅 205 bytes,另有 8 个 dependent 模块合计 336 bytes;
  • Production modeoutput.js 压缩后仅 575 bytes,./example.js 显示 [no exports used]

把这两个数字与同目录下真实的模块代码(205 字节的入口 + 委派包装)对比,可以直观体会到“模块体量几乎全部留在 DLL 中、用户包只保留薄薄一层委派层”的效果——这正是该示例要传达的核心收益:业务包变更时只需要增量重建这层引用,DLL 中占大头的第三方/公共代码无需重新编译。

九、使用要点与边界(结论自查清单)

综合示例与源码,落地“DllPlugin + DllReferencePlugin”方案时请重点核对以下几点:

  1. 路径键必须一致:生产端 manifest 的 content 键与用户端解析出的 libIdent(受用户端 context 影响)必须能精确对上;对无 scope 的 DLL,用户端 context 应指向能还原出 ./alpha.js 这类键的目录(本例为 ../dll);
  2. scope 是用户端视角的命名空间:设置了 scope: "beta" 后,请求必须以 beta/ 开头,插件剥前缀后在 content 中匹配;同名模块如果同时存在于多个无 scope 的 DLL,可能因键冲突而无法区分,这正是示例为 beta 引入 scope 的原因;
  3. extensions 决定命中率:显式列出 DLL 中所含模块的真实扩展名(如 .jsx),否则无扩展名请求可能匹配失败;默认补全集合为 ["", ".js", ".json", ".wasm"]
  4. name 三处必须联动output.library(生产端)、DllPlugin.name(生产端 manifest 内的 name)、用户端通过 manifest 回退得到的 external 名,三处是同一个值;
  5. 加载顺序:DLL 产物必须先于用户包在页面中执行;
  6. 限制DllPlugin 不支持动态 entry;用户端 manifest 读取在 beforeCompile 阶段异步完成,清单缺失/损坏时以编译错误呈现而非直接崩溃。

十、深入阅读:相关源码与配套示例路径

若想继续追踪本文涉及的实现细节,可以按以下路径在仓库中查看:

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

项目优选

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