webpack DLL 构建拆分实战:DllPlugin + DllReferencePlugin 分离 vendor 与应用代码
2026/9/8 21:51:03 网站建设 项目流程

webpack DLL 构建拆分实战:DllPlugin + DllReferencePlugin 分离 vendor 与应用代码

【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack

在大型 webpack 项目中,第三方 vendor 依赖往往体积庞大且几乎不变,如果每次都随业务代码一起参与编译,会显著拖慢开发迭代速度。本指南以仓库中的 dll-app-and-vendor 示例为主线,讲解如何用 DllPlugin 与 DllReferencePlugin 将项目拆成vendor 构建app 构建两条独立编译链路:vendor 只在其依赖列表变化时重新打包一次,平时开发周期中 app 构建直接引用其产出,从而获得更快的应用构建速度。读完本文,你将掌握 vendor DLL 的两个配置文件如何编写、manifest 清单如何串联两条构建、产物代码如何通过全局变量互相引用,以及背后的源码级工作机制。

示例整体思路:把“变”与“不变”分成两次构建

vendor 部分 与 app 部分 是两条互相独立的 webpack 构建,目录结构如下:

examples/dll-app-and-vendor/ ├── 0-vendor/ # 第一次构建:第三方依赖 │ ├── webpack.config.js # DllPlugin 配置 │ └── ... └── 1-app/ # 第二次构建:业务应用 ├── webpack.config.js # DllReferencePlugin 配置 ├── example-app.js └── example.html

核心工作流分两步:

  1. vendor 构建(0-vendor):把第三方依赖(示例中的example-vendor,其内容为导出一个square函数的 ES Module)连同 webpack 内部模块加载机制(__webpack_require__等 runtime)一起打包成独立文件vendor.js,并通过output.library把这段内部 require 机制以全局变量的形式暴露给目标运行环境。
  2. app 构建(1-app):业务代码正常写import { square } from "example-vendor",由 DllReferencePlugin 读取 vendor 构建产出的 manifest 清单,把这些 vendor 模块从 app 编译中彻底剔除,改为在运行时从全局变量(如vendor_lib_xxxx)所代表的 vendor DLL 中按需取出。

这样做的收益正如父级 README 所述:vendor 不再每次都参与 app 的编译,应用构建速度得到提升;同时 vendor DLL 只在 vendor 依赖数组发生变化时才重新构建,而不是在平时的开发循环里反复打包。

阶段一:0-vendor,用 DllPlugin 打出 vendor DLL 与清单

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;

关键配置逐项拆解

配置项示例取值作用与注意事项
entry["example-vendor"]DLL 的入口,通常是一个第三方依赖数组(如 React、Vue、lodash 等),可传多个;在真实项目中往往写成entry: { vendor: ["react", "react-dom"] }这类具名对象形式
output.filenamevendor.js打出的 DLL 文件名,注释提示最好也带[fullhash],便于浏览器缓存
output.libraryvendor_lib_[fullhash]关键点:决定 vendor 构建的产物以什么全局变量名暴露给运行环境,webpack 内部的 require 机制正是挂在这个全局变量下
output.pathdist产物输出目录
new webpack.DllPlugin({...})name+pathname必须与output.library保持一致(这里都用vendor_lib_[fullhash]),保证两条构建能对得上;path指定 manifest 清单的输出位置,即dist/vendor-manifest.json

这里output.libraryDllPluginname都用了占位符[fullhash],因而每次 vendor 内容变化都会得到新的全局变量名与新的 manifest。产物层面(0-vendor/dist/vendor.js)可以看到构建结束时把模块导出挂到了该全局变量上:

// startup 片段 let __webpack_exports__ = __webpack_require__(0); vendor_lib_33d63e473c68d46c4363 = __webpack_exports__;

模块 0 是特殊的 “dll main” 模块,它本身只做一件事——把内部加载函数原样导出:

/* 0 */ /*! dll main !*/ module.exports = __webpack_require__;

这就是“把内部 require 函数暴露为全局变量”的直接体现:模块 0 的内容就是__webpack_require__自身。模块 1 才是真正被入库的example-vendor,它通过__webpack_require__.r(标记为 ES Module)与__webpack_require__.d(定义square的 getter)完成 Harmony 导出。

DllPlugin 的源码工作流

从 lib/dll/DllPlugin.js 的实现可以看到这个插件做了三件事:

  1. entryOption阶段拦截入口(源码lib/dll/DllPlugin.js#L52-L70):把普通对象形式的 entry 逐项包装成内部使用的 DllEntryPlugin,并为每个入口创建对应的 dll 模块;若入口是动态函数形式(entry传 function),插件会直接抛错(目前不支持动态入口)。
  2. 实例化 LibManifestPlugin 来产出 manifest 清单文件。
  3. 通过entryOnly选项控制细节:entryOnly !== false时仅打包入口依赖;当设为false时,还会额外挂载 FlagAllModulesAsUsedPlugin 并强制把模块标记为sideEffectFree = false(见lib/dll/DllPlugin.js#L72-L89),保证副作用代码不会被优化器误删。

manifest 清单:连接两次构建的“地图”

DllPlugin 构建后生成的 0-vendor/dist/vendor-manifest.json 是 app 构建引用 vendor 的唯一凭据,实际内容形如:

{"name":"vendor_lib_33d63e473c68d46c4363","content":{"../node_modules/example-vendor.js":{"id":1,"buildMeta":{"exportsType":"namespace"},"exports":["square"]}}}

其结构为:

  • name:vendor 库的全局变量名(与output.library一致);
  • content:一个“模块真实请求路径 → 内部信息”的映射表,每条包含id(在 vendor DLL 内部模块数组中的索引)、buildMeta.exportsType、以及可用的导出项exports(这里是["square"])。

app 构建正是依据content中“模块路径 → id/导出”的映射,才知道某个 vendor 模块在 DLL 中的位置,从而生成对应的委托加载代码。

阶段二:1-app,用 DllReferencePlugin 消费 vendor DLL

app 部分的配置位于 examples/dll-app-and-vendor/1-app/webpack.config.js:

"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;

业务入口 example-app.js 的写法与不拆 DLL 时完全一致:

import { square } from "example-vendor"; console.log(square(7)); console.log(new square(7));

区别在于:DllReferencePlugin 把example-vendor当作“引用”而非“模块”来处理。插件在beforeCompile阶段读取 manifest(若manifest传的是字符串路径,会通过compiler.inputFileSystem.readFile读取文件再做 JSON 解析,见 lib/dll/DllReferencePlugin.js);随后在compile阶段依据 manifest 内容自动补齐缺失的信息——若配置未显式给出name/sourceType/content,则分别回退取用manifest.namemanifest.typemanifest.content(见lib/dll/DllReferencePlugin.js#L115-L143)。

产物里发生了什么:委托模块(delegated module)

在 1-app/README.md 记录的 app 产物dist/app.js中,example-vendor不再被编译进去,取而代之的是一个委托模块

/* 1 */ /*! delegated ../node_modules/example-vendor.js from dll-reference vendor_lib_33d63e473c68d46c4363 */ module.exports = (__webpack_require__(/*! dll-reference vendor_lib_33d63e473c68d46c4363 */ 2))(1);

以及一个把全局变量直接作为模块导出的“external”:

/* 2 */ /*! external "vendor_lib_33d63e473c68d46c4363" */ module.exports = vendor_lib_33d63e473c68d46c4363;

其中(1)正是 manifest 里记录的 vendor 内部模块 id,而模块 2 拿到的是 vendor 库暴露的整段内部 require 函数。两者拼合的含义是:运行时先取全局变量 vendor 库的 require 函数,再以 id=1 去加载example-vendor——app 自身完全没有打包 vendor 源码。

这一机制在源码层的对应物是 DelegatedModule 与 DelegatedModuleFactoryPlugin:plugin 把 vendor 模块请求解析为DelegatedModule,编译时只保留“来源请求 + 模块 id”这类元信息(DelegatedModule可被序列化缓存),生成阶段再按 manifest 的type(默认"var",即读全局变量)拼接出加载表达式。DllReferencePlugin 还支持scope(把请求限定到某前缀内,见DelegatedModuleFactoryPlugin.js中的 scope 处理)、extensions等扩展选项,可用于更复杂的多 DLL 场景。

运行时:全局变量必须存在

example.html 展示了页面上两条 script 的顺序敏感性——vendor DLL 必须先于 app 加载:

<html> <head></head> <body> <script src="../0-vendor/js/vendor.js" charset="utf-8"></script> <script src="js/app.js" charset="utf-8"></script> </body> </html>

若顺序颠倒,app.js 执行到模块 2 时全局变量vendor_lib_xxx尚不存在,会直接抛 ReferenceError。实际项目可依据各自构建配置的产物路径调整这两个<script>标签(本示例 html 中../0-vendor/js/...与构建配置0-vendor/dist/...的输出目录略有出入,以真实配置产出的路径为准)。

两种模式下的产物规模对照

两个子 README 都记录了webpack X.X.X构建统计信息,展示开发(unoptimized)与生产(production)模式下的体量差异:

vendor 构建(0-vendor)

模式产物关键信息
未优化vendor.js3.49 KiBruntime 614 bytes(3 模块),dll main 12 bytes,依赖模块 45 bytes
productionvendor.js609 bytes(minimized)模块结构同上,压缩后体积骤减

app 构建(1-app)

模式产物关键信息
未优化app.js3.32 KiBruntime 211 bytes,依赖模块 84 bytes(2 个,即委托模块 + external)
productionapp.js335 bytes(minimized)依赖模块 84 bytes,入口 94 bytes

值得注意的是,production 模式下 app 的 runtime 只剩 211 → 0(被移除/合并),而整份 app.js 仅约 335 bytes——相比把 vendor 也一并打进业务包,体积优势非常直观。

在真实项目中的落地建议

基于以上原理与配置,可归纳出几条实践要点:

  1. 何时重建 vendor DLL:仅在 vendor 依赖列表(新增、升级、删除三方包)发生变化时重新执行 vendor 构建,日常开发只跑 app 构建即可获得更快的编译反馈。
  2. 全局变量名保持一致output.libraryDllPluginname必须一致;使用[fullhash]会自动保证每次内容变更后名字随之更新,并避免浏览器/服务器缓存旧 DLL。
  3. manifest 是唯一的桥:app 构建必须能解析到 vendor 构建产出的 manifest(示例中直接require("../0-vendor/dist/vendor-manifest.json"))。注意 DllReferencePlugin 对 manifest 既支持传“已解析的对象”,也支持传“字符串路径”由插件自行读取(后者在 manifest 损坏时会产生DllManifestError编译错误,见 lib/dll/DllReferencePlugin.js),同时会把 manifest 加入fileDependencies,便于 watch 模式下感知其变化。
  4. 入口可传数组或具名对象:真实场景建议使用具名对象(如{ vendor: [...] }),此时 DllPlugin 源码会为每个具名入口各建一个 DLL entry,便于管理多份 vendor 拆分。

如需在本地复现本示例,进入 examples/dll-app-and-vendor 目录后先执行 vendor 构建、再执行 app 构建,两次构建顺序不可颠倒(app 依赖 vendor 先产出 manifest);构建产物与统计信息可分别对照 0-vendor/README.md 与 1-app/README.md 中记录的真实输出。仓库内还提供了更基础的 dll 示例 以及 DllPlugin 相关插件声明,可作为扩展阅读;webpack 官方对 DllPlugin 的更多选项(如contextentryOnly)与 DllReferencePlugin 的scopeextensionssourceType等能力,也可结合 declarations/plugins/dll 的类型定义进一步查阅。

【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询