首页
/ Webpack DLL 分包构建实战:DllPlugin 与 output.library 将 vendor 与 app 分离(dll-app-and-vendor 示例详解)

Webpack DLL 分包构建实战:DllPlugin 与 output.library 将 vendor 与 app 分离(dll-app-and-vendor 示例详解)

2026-09-07 16:55:28作者:侯霆垣

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 源码剖析 DllPluginLibManifestPlugin 的底层实现,最终串起 app 侧的消费流程,形成一套可直接照做的双构建方案。

一、整体思路:为什么要把 vendor 单独构建

examples/dll-app-and-vendor 的示例目标非常明确:将 vendor 与应用(app)拆成两个相互独立的 webpack 构建

它的核心收益是:由于第三方依赖极少变化,把 vendor 单独预编译后,app 的正常开发编译不再重复解析、打包这些库,因此 app 构建更快。示例目录结构(自仓库根)为:

关键流程表述如下(对应 0-vendor 说明):

  1. vendor 与 app 分离构建:vendor 部分独立于 app 编译;按照 DLL 工作流惯例,只有当 vendor 依赖列表发生变化时才需要重新构建 vendor,普通开发周期内通常不会反复执行它(详见 examples/dll-app-and-vendor/0-vendor/template.md)。
  2. 暴露内部 require 为全局变量DllPlugin 配合 output.library 选项,会把 vendor 包内部的模块加载函数 __webpack_require__ 以全局变量的形式暴露到目标运行环境(浏览器/Node 等)。
  3. 生成 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.libraryTargetvar,因此最终产物会创建一个名为 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 暴露的全局变量名,来自 DllPluginname 选项(等于 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):

  1. 校验阶段:在 compiler.hooks.validate 中对传入 options 做 schema 校验,对应校验文件为 schemas/plugins/dll/DllPlugin.json
  2. 入口处理阶段:在 entryOption 钩子中,为配置的每个入口名创建一个 DllEntryPlugin,由其构造 DLL 入口模块(“dll main”,即产物中的模块 0);若传入的是动态入口函数,则直接抛出 “DllPlugin doesn't support dynamic entry (function) yet”(见 lib/dll/DllPlugin.js)。
  3. 清单产出阶段:创建 LibManifestPlugin({ ...this.options, entryOnly }),负责在 emit 时生成 manifest。
  4. 可选的全量标记阶段:当 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 main 12 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 工作流浓缩为可直接照做的清单:

  1. vendor 单独构建:给 vendor 配置一套独立 config,入口填所有第三方依赖数组;output.libraryDllPluginname 使用同一条带 [fullhash] 的名称;DllPluginpath 指向 manifest 输出位置。
  2. 仅在依赖变更时重build:vendor DLL 内容稳定,适合独立于日常开发周期构建并长期缓存。
  3. app 侧引用 manifest:用 DllReferencePluginmanifest 选项加载 vendor-manifest.json,webpack 即会据此排除 vendor 模块并把引用委托给全局 require。
  4. 保证脚本顺序:HTML 中必须先加载 vendor.js 再加载 app.js,否则 app 运行时拿不到全局变量。
  5. 理解产物三元组:vendor 产物 = 模块表(含 id 0 “dll main”)+ webpack 运行时(挂全局)+ manifest(模块名→id/导出 映射);app 产物中 vendor 相关代码退化为一行委托与一行 external。

如需继续深入源码,推荐依次阅读:lib/dll/DllPlugin.jslib/dll/DllEntryPlugin.jslib/dll/LibManifestPlugin.jslib/dll/DllReferencePlugin.js,以及 lib/dll/DllModule.jslib/dll/DelegatedModule.js 等配套模块,即可完整还原从“预编译 vendor”到“app 委托取模块”的整条链路。

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