Webpack DLL 分包构建实战:DllPlugin 与 output.library 将 vendor 与 app 分离(dll-app-and-vendor 示例详解)
DLL(Dynamic Link Library)分包是 webpack 为“第三方库基本不变、应用代码频繁改动”场景设计的一类拆分方案:先把 vendor(如 React、jQuery 等不常变化的依赖)单独打成一个 vendor.js 并在启动时挂到全局,再让 app 构建通过 DllReferencePlugin 消费一份 manifest,从而把 vendor 模块从 app 编译中整体剔除、显著压缩日常开发构建的编译范围。本文以仓库示例 examples/dll-app-and-vendor 的 vendor 一侧(即 examples/dll-app-and-vendor/0-vendor)为绝对主体,完整讲解 vendor DLL 的配置、产物结构、manifest 内容与输出统计,并结合 lib/dll 源码剖析 DllPlugin → LibManifestPlugin 的底层实现,最终串起 app 侧的消费流程,形成一套可直接照做的双构建方案。
一、整体思路:为什么要把 vendor 单独构建
examples/dll-app-and-vendor 的示例目标非常明确:将 vendor 与应用(app)拆成两个相互独立的 webpack 构建。
它的核心收益是:由于第三方依赖极少变化,把 vendor 单独预编译后,app 的正常开发编译不再重复解析、打包这些库,因此 app 构建更快。示例目录结构(自仓库根)为:
- examples/dll-app-and-vendor/0-vendor:vendor 构建侧,产出
vendor.js与vendor-manifest.json; - examples/dll-app-and-vendor/1-app:app 构建侧,消费上面的 DLL。
关键流程表述如下(对应 0-vendor 说明):
- vendor 与 app 分离构建:vendor 部分独立于 app 编译;按照 DLL 工作流惯例,只有当 vendor 依赖列表发生变化时才需要重新构建 vendor,普通开发周期内通常不会反复执行它(详见 examples/dll-app-and-vendor/0-vendor/template.md)。
- 暴露内部 require 为全局变量:
DllPlugin配合output.library选项,会把 vendor 包内部的模块加载函数__webpack_require__以全局变量的形式暴露到目标运行环境(浏览器/Node 等)。 - 生成 manifest:构建同时生成一份 manifest(清单),记录“模块名 → 内部模块 id”的映射,供 app 侧解析使用。
也就是说,产物侧存在两样东西:可被直接 <script> 引用的 DLL 文件,以及描述它的清单文件。
二、vendor 侧配置逐项解读
vendor 侧真实配置见 examples/dll-app-and-vendor/0-vendor/webpack.config.js,全文如下:
"use strict";
const path = require("path");
const webpack = require("../../../");
/** @type {import("webpack").Configuration} */
const config = {
// mode: "development" || "production",
context: __dirname,
entry: ["example-vendor"],
output: {
filename: "vendor.js", // best use [fullhash] here too
path: path.resolve(__dirname, "dist"),
library: "vendor_lib_[fullhash]"
},
plugins: [
new webpack.DllPlugin({
name: "vendor_lib_[fullhash]",
path: path.resolve(__dirname, "dist/vendor-manifest.json")
})
]
};
module.exports = config;
各配置项的含义与注意点:
| 配置项 | 值 | 作用与说明 |
|---|---|---|
mode |
注释为 "development" / "production" |
示例不强制指定;两种模式产物尺寸对比见文末“构建统计”。 |
context |
__dirname |
设置模块解析的基础目录,保证 manifest 中的模块标识基于 vendor 目录解析。 |
entry |
["example-vendor"] |
vendor 依赖入口。example-vendor 是一个占位性的第三方模块(在示例构建中解析为 node_modules/example-vendor.js,其源码仅导出一个 square 函数),在实际项目中替换为你的真实第三方依赖数组即可。 |
output.filename |
"vendor.js" |
注释明确建议:这里最好同样使用 [fullhash](如 vendor.[fullhash].js),以配合下方基于 hash 的全局变量名,便于长效缓存与版本区分。 |
output.path |
path.resolve(__dirname, "dist") |
vendor 产物输出目录。 |
output.library |
"vendor_lib_[fullhash]" |
DLL 能否被 app 引用的关键。默认 output.libraryTarget 为 var,因此最终产物会创建一个名为 vendor_lib_<fullhash> 的全局变量,指向 vendor 包内部的 __webpack_require__。 |
plugins[0] |
new webpack.DllPlugin({...}) |
核心插件,name 必须与 output.library 保持一致(同样带 [fullhash]),path 指定 manifest 输出位置(绝对路径)。 |
实际输出中出现的
vendor_lib_33d63e473c68d46c4363只是本示例某次构建生成的演示 hash;由于名称里带[fullhash],不同内容/版本会得到不同的变量名,运行时以自己构建出的实际名称为准。
关于 name/output.library 保持一致的原因
从仓库实现看,DllPlugin 会在内部创建 LibManifestPlugin,并把上述选项透传给它(见 lib/dll/DllPlugin.js)。LibManifestPlugin 定义里对 name 的说明是 “Name of the exposed dll function (external name, use value of 'output.library')”,即 manifest 中记录的名字应当与 output.library 一致,app 侧正是依靠这个名字去定位全局变量、把模块加载委托给该 DLL(见 lib/dll/LibManifestPlugin.js)。因此约定俗成的做法是两侧都写成 vendor_lib_[fullhash]。
三、vendor 源码与打包结构:从 square 到 DLL 模块表
3.1 入模块源码
vendor 的唯一内容模块 example-vendor(构建后被注释为 ../node_modules/example-vendor.js)是典型的最小 ESM 模块:
export function square(n) {
return n * n;
}
3.2 产物 dist/vendor.js 剖析
未压缩的产物整体是一个自执行函数(webpackBootstrap),内部维护模块表 __webpack_modules__,此处节选两个关键模块:
var vendor_lib_33d63e473c68d46c4363;
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ([
/* 0 */
/*!****************!*\
!*** dll main ***!
\****************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
module.exports = __webpack_require__;
/***/ }),
/* 1 */
/*!*****************************************!*\
!*** ../node_modules/example-vendor.js ***!
\*****************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ square: () => (/* binding */ square)
/* harmony export */ });
function square(n) {
return n * n;
}
/***/ })
/******/ ]);
其中隐藏着 DLL 机制的两个关键设计:
- 模块 id 0(dll main):一个特殊入口模块,其代码就是
module.exports = __webpack_require__;,即把 webpack 运行时内部的模块加载函数自身当作模块导出; - 模块 id 1(真正的 vendor 模块):
example-vendor被正常打包,square通过__webpack_require__.d定义到命名空间导出上。
产物末尾的 startup 逻辑把二者串联起来(对应原文档中的启动段):
// startup
let __webpack_exports__ = __webpack_require__(0);
vendor_lib_33d63e473c68d46c4363 = __webpack_exports__;
它执行模块 0 拿到 __webpack_require__,然后把它赋给全局变量 vendor_lib_xxx。这正是第一节所描述的机制落地:全局变量本质上就是 vendor DLL 的内部 require 函数,任何拿到它的调用方都可以用模块 id 直接索取 DLL 中的任意模块。
模块表之外,产物中还内嵌了 webpack 运行时辅助函数(runtime code,位于注释 <details> 展开的源码里),包括:
- 模块缓存
__webpack_module_cache__与__webpack_require__(moduleId)主函数; __webpack_require__.d(define property getters):为 harmony 导出定义 getter;__webpack_require__.o(hasOwnProperty shorthand);__webpack_require__.r(make namespace object):给导出对象打上__esModule/Symbol.toStringTag标记。
注意:模块 id 1 的导出注释为 [provided] [no usage info],而导出项 square 被标记为 [provided]。这是因为 vendor 侧单独构建时并不知道 app 究竟会用到哪些导出,需在 manifest 里如实记录可提供的导出集合(exports: ["square"]),把“具体用哪些”的决定权留给 app 构建。
四、manifest:模块名到内部 id 的映射清单
构建成功后,DllPlugin 会在 dist 下额外生成 dist/vendor-manifest.json。原文档中给出的是压缩单行 JSON,格式化后可读为:
{
"name": "vendor_lib_33d63e473c68d46c4363",
"content": {
"../node_modules/example-vendor.js": {
"id": 1,
"buildMeta": { "exportsType": "namespace" },
"exports": ["square"]
}
}
}
三个字段的含义:
| 字段 | 含义 |
|---|---|
name |
暴露的全局变量名,来自 DllPlugin 的 name 选项(等于 output.library 经 [fullhash] 替换后的结果),app 侧据此引用全局。 |
content 的 key |
模块的库标识 libIdent(这里基于 context 解析为 ../node_modules/example-vendor.js),也就是“模块名”。 |
content 的 value |
模块内部信息:id(在 DLL 模块表中的编号 1)、buildMeta(构建元信息,此处导出类型为 namespace)、exports(该模块提供的导出名列表 ["square"])。 |
这一结构可以从 lib/dll/LibManifestPlugin.js 的实现直接印证:插件遍历初始 chunk 中的模块,调用 module.libIdent() 取标识作为 key,读取 chunkGraph.getModuleId(module) 得到 id,并通过模块图的 ExportsInfo.getProvidedExports() 收集提供的导出,最终组装成 { name, type, content } 形式输出。此外,LibManifestPluginOptions 还支持若干可选参数(详见 lib/dll/LibManifestPlugin.js):
context:manifest 中请求路径的解析上下文(默认取 webpack context);entryOnly:为true时只暴露入口模块(默认true);format:为true时对 JSON 做美化格式化输出;type:DLL 包的外部类型(对应output.libraryTarget);name/path:全局变量名与 manifest 绝对输出路径。
插件在编译器 emit 阶段(stage: 110)异步生成该文件,写文件前会 mkdirp 创建目标目录;如果多个 chunk 解析到同一目标路径,会报错 “each chunk must have a unique path”(见 lib/dll/LibManifestPlugin.js)。
五、DllPlugin 底层:它实际替你做了什么
DllPlugin 本身并不“生产” DLL,它本质上是几个子插件的组合与调度(源码见 lib/dll/DllPlugin.js):
- 校验阶段:在
compiler.hooks.validate中对传入 options 做 schema 校验,对应校验文件为 schemas/plugins/dll/DllPlugin.json。 - 入口处理阶段:在
entryOption钩子中,为配置的每个入口名创建一个DllEntryPlugin,由其构造 DLL 入口模块(“dll main”,即产物中的模块 0);若传入的是动态入口函数,则直接抛出 “DllPlugin doesn't support dynamic entry (function) yet”(见 lib/dll/DllPlugin.js)。 - 清单产出阶段:创建
LibManifestPlugin({ ...this.options, entryOnly }),负责在 emit 时生成 manifest。 - 可选的全量标记阶段:当
entryOnly: false时,追加FlagAllModulesAsUsedPlugin,并把模块标记为有副作用(factoryMeta.sideEffectFree = false),避免 tree-shaking 误删“虽然未被当前入口直接引用、但需在 DLL 中保留”的模块(见 lib/dll/DllPlugin.js)。
也就是说,配置 DllPlugin 后你拿到的是:一个“dll main”入口 + 常规的 vendor 打包 + 一份 lib manifest,三件事协同完成“预编译 + 注册表导出”。
六、联动 app 侧:DllReferencePlugin 如何消费 DLL
虽然本文主体是 0-vendor,但要理解 vendor 产物为何如此设计,离不开 app 侧的消费环节(examples/dll-app-and-vendor/1-app/webpack.config.js):
const manifest = "../0-vendor/dist/vendor-manifest.json";
const config = {
context: __dirname,
entry: "./example-app",
output: {
filename: "app.js",
path: path.resolve(__dirname, "dist")
},
plugins: [
new webpack.DllReferencePlugin({
manifest: require(manifest)
})
]
};
app 源码 examples/dll-app-and-vendor/1-app/example-app.js 直接 import { square } from "example-vendor" 并调用。构建时 DllReferencePlugin 读取 manifest 中 content 里的模块名映射,识别出 example-vendor 属于 DLL 内容,从而:
- 把 vendor 模块从 app 编译中排除,不重复解析和打包;
- 在 app 产物中生成一个 delegated(委托)模块,把对该模块的请求改写为“先拿全局变量
vendor_lib_xxx(即 vendor 的内部 require),再用模块 id 1 取模块”:
module.exports = (__webpack_require__(/*! dll-reference vendor_lib_33d63e473c68d46c4363 */ 2))(1);
其中模块 2 是一个 external,即:
module.exports = vendor_lib_33d63e473c68d46c4363;
app 侧产物体积的对比可以直观说明收益:vendor.js 未压缩约 3.49 KiB,而 app.js 未压缩仅约 3.32 KiB(其中主体是 webpack 运行时,几乎不含第三方库代码)。实际浏览器中的加载顺序必须保证 DLL 先于 app(examples/dll-app-and-vendor/1-app/example.html):
<script src="../0-vendor/js/vendor.js" charset="utf-8"></script>
<script src="js/app.js" charset="utf-8"></script>
七、构建统计:两种 mode 下的产物表现
原文档在末尾给出了两次构建(开发与生产)的 stats 节选,信息整理如下:
Unoptimized(开发模式)
asset vendor.js 3.49 KiB [emitted] (name: main)
chunk (runtime: main) vendor.js (main) 57 bytes (javascript) 614 bytes (runtime) [entry] [rendered]
> main
runtime modules 614 bytes 3 modules
dependent modules 45 bytes [dependent] 1 module
dll main 12 bytes [built] [code generated]
[used exports unknown]
dll entry
used as library export
webpack X.X.X compiled successfully
Production mode(生产模式)
asset vendor.js 609 bytes [emitted] [minimized] (name: main)
chunk (runtime: main) vendor.js (main) 57 bytes (javascript) 614 bytes (runtime) [entry] [rendered]
> main
runtime modules 614 bytes 3 modules
dependent modules 45 bytes [dependent] 1 module
dll main 12 bytes [built] [code generated]
dll entry
used as library export
webpack X.X.X compiled successfully
解读要点:
- 两种模式下
vendor.js从 3.49 KiB 压缩到 609 bytes(生产模式标记[minimized]); - chunk 本体只有 57 bytes(
dll main12 bytes + 依赖模块 45 bytes),大头 614 bytes 属于 runtime modules(即前面第 3.2 节列出的__webpack_require__、.d、.o、.r等运行时辅助); - “dll entry”“used as library export”两条注释分别表明模块 0 承担 DLL 入口职责、且被当作库导出暴露;
- 由于
name/library带[fullhash],当 vendor 内容变化时全局变量名与文件名(若配置[fullhash])都会随之更新,避免浏览器缓存了旧 DLL 而导致 app 取到过期模块表。
八、实践要点小结
把整个 vendor DLL 工作流浓缩为可直接照做的清单:
- vendor 单独构建:给 vendor 配置一套独立 config,入口填所有第三方依赖数组;
output.library与DllPlugin的name使用同一条带[fullhash]的名称;DllPlugin的path指向 manifest 输出位置。 - 仅在依赖变更时重build:vendor DLL 内容稳定,适合独立于日常开发周期构建并长期缓存。
- app 侧引用 manifest:用
DllReferencePlugin的manifest选项加载vendor-manifest.json,webpack 即会据此排除 vendor 模块并把引用委托给全局 require。 - 保证脚本顺序:HTML 中必须先加载
vendor.js再加载app.js,否则 app 运行时拿不到全局变量。 - 理解产物三元组:vendor 产物 = 模块表(含 id 0 “dll main”)+ webpack 运行时(挂全局)+ manifest(模块名→id/导出 映射);app 产物中 vendor 相关代码退化为一行委托与一行 external。
如需继续深入源码,推荐依次阅读:lib/dll/DllPlugin.js、lib/dll/DllEntryPlugin.js、lib/dll/LibManifestPlugin.js、lib/dll/DllReferencePlugin.js,以及 lib/dll/DllModule.js、lib/dll/DelegatedModule.js 等配套模块,即可完整还原从“预编译 vendor”到“app 委托取模块”的整条链路。
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