用 webpack DllPlugin 拆分 vendor 与应用构建:官方 dll-app-and-vendor 示例深度解析
在大型 webpack 项目中,第三方依赖(vendor)往往数量庞大且极少变动,如果让每次应用构建都重新解析、打包这些依赖,将显著拖慢开发迭代速度。webpack 官方仓库中的 dll-app-and-vendor 示例 正是为此给出的一套经典实践:把工程拆成"vendor 构建"与"app 构建"两个独立部分,前者借助 DllPlugin 将依赖打包成可复用、带清单(manifest)的预编译产物,后者借助 DllReferencePlugin 直接引用该产物。本文以其中 0-vendor 部分(vendor 构建)为核心骨架,逐行拆解其 webpack.config.js、构建产物与 manifest 结构,并下沉到 lib/dll 源码层解释 DllPlugin 的注册与生成机制,帮助你理解并复现这套"DLL 预编译 + 清单引用"的加速方案。
一、示例整体结构与设计动机
整个示例位于仓库的 examples/dll-app-and-vendor 目录,由两个相对独立的构建单元组成:
| 目录 | 角色 | 关键文件 |
|---|---|---|
| examples/dll-app-and-vendor/0-vendor | vendor 构建方 | webpack.config.js,输出 dist/vendor.js 与 dist/vendor-manifest.json |
| examples/dll-app-and-vendor/1-app | 应用构建方 | webpack.config.js,消费 manifest,输出 dist/app.js |
正如 examples/dll-app-and-vendor/README.md 所述,这样做的核心收益是显著加速应用构建:vendor 不再参与应用编译,而是被预先独立构建;此后只有在 vendor 列表发生变化时才需要重建 vendor dll,普通开发周期内完全不触碰它(原文档原话:"only built when the array of vendors has changed and not during the normal development cycle")。
在进入配置之前,需要先厘清一个关键分工:vendor 部分负责"制造 DLL 与清单",app 部分负责"按清单引用 DLL"。本文主体聚焦 vendor 侧 DllPlugin 的完整机制,同时在第五节补充 app 侧如何消费产物,以还原端到端闭环。
二、vendor 构建配置逐行拆解(webpack.config.js)
0-vendor 部分的 webpack.config.js 全文如下,是理解 DllPlugin 用法的最小可运行范本:
"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;
2.1 entry: ["example-vendor"]:用模块请求数组描述 vendor 集合
与常见 entry 用法不同,这里 entry 是一个字符串数组形式的模块请求列表("example-vendor" 解析到 node_modules/example-vendor.js,该文件在 examples/dll-app-and-vendor/0-vendor/template.md 中被完整展示,仅导出一个 square(n) 函数)。这种写法表达的语义是:"把以下这些模块整体收进同一个 DLL"。在你的真实项目中,只需把 entry 换成依赖清单即可,例如 entry: ["react", "react-dom", "lodash"]。
在源码层面,这一语义由 lib/dll/DllPlugin.js 在 compiler.hooks.entryOption 阶段落实:DllPlugin 遍历 entry 中每个名称,为每一项实例化一个 DllEntryPlugin(位于同一 lib/dll 目录),把每个 vendor 模块注册成"dll entry"。代码同时限制:DllPlugin 不支持函数形式的动态入口(doesn't support dynamic entry (function) yet,见 lib/dll/DllPlugin.js),这也是 vendor 清单通常保持稳定的原因之一。
2.2 output.library: "vendor_lib_[fullhash]":把内部 require 暴露为全局变量
这是整个方案的关键机制。原文档明确指出:
The DllPlugin in combination with the
output.libraryoption exposes the internal require function as global variable in the target environment.
即:DLL 产物本质上仍是一个 webpack bundle,模块仍由内部的 __webpack_require__ 加载;而 output.library 会把内部 require 函数本身作为模块 0 的导出,再整体赋值给一个全局变量。app 侧后续引用 DLL 时,实际上拿到的是一个"活的 require",通过模块 id 从 DLL 内取模块。
[fullhash] 占位符让每次内容变化后全局变量名随之变化(对应源码注释 "best use [fullhash] here too"),与文件名、manifest 中的 name 保持一致,避免旧版本全局变量互相污染。
2.3 DllPlugin 的两个配置项
new webpack.DllPlugin({
name: "vendor_lib_[fullhash]", // 与 output.library 必须一致
path: path.resolve(__dirname, "dist/vendor-manifest.json")
})
| 配置项 | 说明 | 取值要点 |
|---|---|---|
name |
暴露出的 DLL 全局变量名(manifest 顶层 name 字段的来源) |
必须与 output.library 保持一致,支持 [fullhash] 等编译期占位符 |
path |
manifest 文件的绝对输出路径 | 通常放于 output.path 内,供 app 构建侧以 JSON 方式读取 |
DllPlugin 的其余可选配置(如 context、entryOnly、format、type)可以通过 declarations/plugins/dll/DllPlugin.d.ts 与 schemas/plugins/dll/DllPlugin.json 查看完整的校验约束。其中两个值得注意:
entryOnly(默认true):只把入口模块写入 manifest。若设为false,DllPlugin 会额外注入 FlagAllModulesAsUsedPlugin 并把所有模块标记为非副作用自由(见 lib/dll/DllPlugin.js),以保证被引用的内部模块不被 tree shaking 移除。- DllPlugin 的合法输入会在
compiler.hooks.validate阶段通过 schema 校验,非法配置会直接报错(lib/dll/DllPlugin.js)。
2.4 构建产物与占位符的一致性约定
0-vendor 构建后,dist/ 下会出现两个相互关联的文件:
vendor.js—— 可被页面直接<script>加载的 DLL bundle(建议使用[fullhash]文件名以配合强缓存);vendor-manifest.json—— 模块名 → 内部 id 的映射清单,是 app 构建侧的唯一"接口契约"。
三、产物形态:vendor.js 与 vendor-manifest.json 里到底有什么
原文档用四个代码块完整展示了 vendor 部分的构建产物,下文逐一解读(示例产物中的 hash 为 33d63e473c68d46c4363,实际构建时会随内容变化)。
3.1 dist/vendor.js 的三段式结构
产物开头的全局声明与 bootstrap 骨架如下(示意,完整代码见原文档/README):
var vendor_lib_33d63e473c68d46c4363;
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ([
/* 0 */ // !*** dll main ***!
// module.exports = __webpack_require__;
/* 1 */ // !*** ../node_modules/example-vendor.js ***!
// __webpack_require__.r/__webpack_require__.d 定义 square 导出
/******/ ]);
// ... webpack runtime(__webpack_require__、模块缓存、.d/.o/.r 帮助函数)
/******/ let __webpack_exports__ = __webpack_require__(0);
/******/ vendor_lib_33d63e473c68d46c4363 = __webpack_exports__;
/******/ })();
可以看到三个关键结构:
- 模块 0(dll main):它的导出就是
__webpack_require__本身,即整个 DLL 的运行内核; - 模块 1 及之后:真正被打包的 vendor 模块,这里
example-vendor.js被转换成标准的 ES module(__webpack_require__.r标记 namespace,__webpack_require__.d为square定义 getter); - 收尾赋值:
__webpack_require__(0)的结果被赋给全局vendor_lib_[fullhash],DLL 对外"窗口"就此打开。注意这里用的是直接赋值而非 var 声明初始化,因此要求vendor.js必须先于 app 代码加载执行。
3.2 dist/vendor-manifest.json 的数据结构
原文档给出的 manifest 如下(单行 JSON):
{"name":"vendor_lib_33d63e473c68d46c4363","content":{"../node_modules/example-vendor.js":{"id":1,"buildMeta":{"exportsType":"namespace"},"exports":["square"]}}}
结构可拆解为:
- 顶层
name:对应DllPlugin.options.name(本例为vendor_lib_[fullhash]),app 侧依赖它定位全局变量; - 顶层
content:以模块标识符(module.libIdent(),相对 context 的解析名)为键、以模块元数据为值的映射,每个值包含:id:模块在 DLL bundle 内部的模块编号(manifest 中的 id 与vendor.js的模块序号一一对应);buildMeta:如exportsType: "namespace",描述模块的导出方式;exports:可静态分析的导出名数组(如["square"]),供 app 侧做精细的具名引用。
这份映射由 lib/dll/LibManifestPlugin.js 在 compiler.hooks.emit 阶段生成:它遍历所有 initial chunk 的模块,通过 chunkGraph.getModuleId 取得内部 id、通过 moduleGraph 的 getProvidedExports 收集导出信息,最终写入 manifest 文件(见 lib/dll/LibManifestPlugin.js)。这也解释了为什么 manifest 里的 id 能与 vendor.js 中的模块数组序号精确对得上——两者来自同一次编译的 ChunkGraph。
四、从源码看 DllPlugin 的完整生效链路
把 lib/dll/DllPlugin.js 的 apply 方法串起来,可以得到 vendor 构建在 webpack 生命周期中的完整动作序列:
- validate 阶段:用
schemas/plugins/dll/DllPlugin.json校验name、path等选项合法性; - entryOption 阶段:为 entry 中每个 vendor 请求创建
DllEntryPlugin(把依赖标记为 DLL 入口,产出dll main模块 0),并返回true阻止后续 EntryOptionPlugin 再创建普通入口,从而让"vendor.js 只含 dll main + vendor 模块"; - 插件装配:实例化并挂载
LibManifestPlugin,同时把entryOnly(默认true)透传进去; - emit 阶段:
LibManifestPlugin遍历 chunk 内模块,生成{ name, content, ... }清单并写盘。
编译器的 context 与 manifest 中模块标识符的解析基准均来自配置的 context: __dirname(即 0-vendor 目录),因此清单中 example-vendor.js 显示为 ../node_modules/example-vendor.js 这样的相对路径。
值得一提的是,仓库中还有一组姊妹示例,分别演示了 DLL 的不同组织方式,适合交叉阅读:
- examples/dll:多入口 DLL(
alpha/beta),演示[name]与[name]-manifest.json的组合用法; - examples/dll-entry-only:聚焦
entryOnly相关行为; - examples/dll-user:展示与上述例子配套的引用端。
五、闭环:1-app 侧如何消费 manifest 与 DLL
vendor 构建完成后,app 构建配置 变得极轻:
"use strict";
const path = require("path");
const webpack = require("../../../");
const manifest = "../0-vendor/dist/vendor-manifest.json";
/** @type {import("webpack").Configuration} */
const config = {
// mode: "development" || "production",
context: __dirname,
entry: "./example-app",
output: {
filename: "app.js",
path: path.resolve(__dirname, "dist")
},
plugins: [
new webpack.DllReferencePlugin({
manifest: require(manifest)
})
]
};
module.exports = config;
如 app 部分文档 所述,DllReferencePlugin 读取 manifest 后完成两件事:
- 把 manifest 中列出的所有 vendor 模块从应用编译中排除——webpack 不再解析、转换、打包
example-vendor,这是应用构建提速的直接来源; - 把对 vendor 模块的引用改写为"从全局变量加载":例如 example-app.js 中
import { square } from "example-vendor"最终会转换为从vendor_lib_xxxx这个全局 require 中按 id 取模块,再访问具名导出square。
由于全局变量名内含 [fullhash],app 侧必须能拿到与 vendor.js 一致的名称——这正是 manifest 顶层 name 字段存在的意义:DllReferencePlugin 从 manifest 读取 name 而非在 app 配置里硬编码,保证 hash 变化后引用端自动跟随。
运行时加载顺序见 example.html,必须先加载 vendor.js,再加载 app.js:
<script src="../0-vendor/js/vendor.js" charset="utf-8"></script>
<script src="js/app.js" charset="utf-8"></script>
六、构建信息与体积对比(Info 输出解读)
0-vendor 的 README 末尾给出了开发(Unoptimized)与生产(Production mode)两种模式的真实构建输出,可作为验证与体感参考:
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
Production mode(生产模式,启用压缩)
asset vendor.js 609 bytes [emitted] [minimized] (name: main)
两相对照可以得到几个信息:dll main 仅有 12 bytes(因为它只负责 module.exports = __webpack_require__),打包体积主要来自 runtime(614 bytes,即 .d/.o/.r 等帮助函数)与 vendor 模块本体;生产模式下 Terser 会把 bundle 从 3.49 KiB 压到 609 bytes。配置中的 // mode: "development" || "production" 注释提示了这两类输出对应 mode 取值;运行 vendor 构建时建议先确定 mode 再构建,因为 [fullhash] 会随产物内容变化,开发与生产环境产物不可混用。
七、落地到真实项目的操作要点与注意事项
综合 0-vendor 模板文档 与源码实现,把一个真实的 vendor 拆分方案落地时,可以遵循以下步骤与约束:
推荐实施顺序
- 规划 vendor 清单:选定不常变化的第三方依赖(如 React、工具库),作为 vendor 构建的
entry数组;example-vendor中的依赖更新就是触发 DLL 重建的信号。 - 独立配置 vendor 构建:复制 examples/dll-app-and-vendor/0-vendor/webpack.config.js 的骨架,设置
output.library(建议带[fullhash])、output.filename、DllPlugin.path与一致的name。 - 生成并检视产物:构建后在 dist 中核对
vendor.js与vendor-manifest.json同时存在,且 manifest 的name、各模块id与vendor.js对应。 - 应用侧接入:按 examples/dll-app-and-vendor/1-app/webpack.config.js 配置
DllReferencePlugin,通过require读取 manifest;HTML 中先引 DLL 后引应用。 - 开发流程定型:日常只构建 app;仅在 vendor 清单或依赖版本变化时重跑 vendor 构建,从而把"昂贵的依赖打包"从开发循环中剥离。
易错点速查
output.library与DllPlugin.options.name不一致会导致引用端找不到全局变量;output.libraryTarget/type需要匹配 app 侧期望的全局变量形式(本示例使用默认的 global/library 形态);- manifest 的
path必须使用绝对路径,且 0-vendor 与 1-app 需就 manifest 位置与文件名达成一致(示例通过相对路径../0-vendor/dist/vendor-manifest.json引用); - 引入新 vendor 模块但忘重建 DLL 时,引用端会因 manifest 中缺少对应模块而报解析错误——这是"何时重建"最直观的判据;
[fullhash]参与全局变量名后,必须保证旧版本vendor.js不会在 CDN/缓存中与新 manifest 混用,否则会出现 hash 对不上的运行时错误。
结语
从 0-vendor 模板 出发可以看到,webpack 的 DLL 方案本质上是用"两次构建 + 一份清单"换取了应用构建的高频提速:第一次构建用 DllPlugin 把 vendor 打包为带全局 require 的 bundle 并吐出 vendor-manifest.json,此后 app 构建经 DllReferencePlugin 读取清单,把 vendor 完全排除在编译之外。理解 output.library 与 __webpack_require__ 的关系、manifest 中 name/content/id/exports 的语义,以及 entryOnly 等选项在 lib/dll/DllPlugin.js 与 lib/dll/LibManifestPlugin.js 中的实现,是掌握整套机制的关键。需要更深层机制与多入口/多 DLL 编排时,可以继续研读仓库内 examples/dll、examples/dll-entry-only 与 examples/dll-user 等配套示例。
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