首页
/ 用 webpack DllPlugin 拆分 vendor 与应用构建:官方 dll-app-and-vendor 示例深度解析

用 webpack DllPlugin 拆分 vendor 与应用构建:官方 dll-app-and-vendor 示例深度解析

2026-09-07 19:29:43作者:齐添朝

在大型 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.jsdist/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.jscompiler.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.library option 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 的其余可选配置(如 contextentryOnlyformattype)可以通过 declarations/plugins/dll/DllPlugin.d.tsschemas/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/ 下会出现两个相互关联的文件:

  1. vendor.js —— 可被页面直接 <script> 加载的 DLL bundle(建议使用 [fullhash] 文件名以配合强缓存);
  2. 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__.dsquare 定义 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.jscompiler.hooks.emit 阶段生成:它遍历所有 initial chunk 的模块,通过 chunkGraph.getModuleId 取得内部 id、通过 moduleGraph 的 getProvidedExports 收集导出信息,最终写入 manifest 文件(见 lib/dll/LibManifestPlugin.js)。这也解释了为什么 manifest 里的 id 能与 vendor.js 中的模块数组序号精确对得上——两者来自同一次编译的 ChunkGraph。

四、从源码看 DllPlugin 的完整生效链路

lib/dll/DllPlugin.jsapply 方法串起来,可以得到 vendor 构建在 webpack 生命周期中的完整动作序列:

  1. validate 阶段:用 schemas/plugins/dll/DllPlugin.json 校验 namepath 等选项合法性;
  2. entryOption 阶段:为 entry 中每个 vendor 请求创建 DllEntryPlugin(把依赖标记为 DLL 入口,产出 dll main 模块 0),并返回 true 阻止后续 EntryOptionPlugin 再创建普通入口,从而让"vendor.js 只含 dll main + vendor 模块";
  3. 插件装配:实例化并挂载 LibManifestPlugin,同时把 entryOnly(默认 true)透传进去;
  4. emit 阶段LibManifestPlugin 遍历 chunk 内模块,生成 { name, content, ... } 清单并写盘。

编译器的 context 与 manifest 中模块标识符的解析基准均来自配置的 context: __dirname(即 0-vendor 目录),因此清单中 example-vendor.js 显示为 ../node_modules/example-vendor.js 这样的相对路径。

值得一提的是,仓库中还有一组姊妹示例,分别演示了 DLL 的不同组织方式,适合交叉阅读:

五、闭环: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 后完成两件事:

  1. 把 manifest 中列出的所有 vendor 模块从应用编译中排除——webpack 不再解析、转换、打包 example-vendor,这是应用构建提速的直接来源;
  2. 把对 vendor 模块的引用改写为"从全局变量加载":例如 example-app.jsimport { 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 拆分方案落地时,可以遵循以下步骤与约束:

推荐实施顺序

  1. 规划 vendor 清单:选定不常变化的第三方依赖(如 React、工具库),作为 vendor 构建的 entry 数组;example-vendor 中的依赖更新就是触发 DLL 重建的信号。
  2. 独立配置 vendor 构建:复制 examples/dll-app-and-vendor/0-vendor/webpack.config.js 的骨架,设置 output.library(建议带 [fullhash])、output.filenameDllPlugin.path 与一致的 name
  3. 生成并检视产物:构建后在 dist 中核对 vendor.jsvendor-manifest.json 同时存在,且 manifest 的 name、各模块 idvendor.js 对应。
  4. 应用侧接入:按 examples/dll-app-and-vendor/1-app/webpack.config.js 配置 DllReferencePlugin,通过 require 读取 manifest;HTML 中先引 DLL 后引应用。
  5. 开发流程定型:日常只构建 app;仅在 vendor 清单或依赖版本变化时重跑 vendor 构建,从而把"昂贵的依赖打包"从开发循环中剥离。

易错点速查

  • output.libraryDllPlugin.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.jslib/dll/LibManifestPlugin.js 中的实现,是掌握整套机制的关键。需要更深层机制与多入口/多 DLL 编排时,可以继续研读仓库内 examples/dllexamples/dll-entry-onlyexamples/dll-user 等配套示例。

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

项目优选

收起
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