webpack 实战:基于 DllReferencePlugin 的应用端接入——复用预构建 vendor 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
本指南以仓库中的 examples/dll-app-and-vendor/1-app/template.md 为主体,结合同目录 0-vendor 一侧与 lib/dll 下的真实实现,完整讲解“DLL 分包”模式中**应用端(app part)**如何接入。你将掌握:DllReferencePlugin如何读取 manifest 并让 vendor 模块不再进入应用编译、应用侧代码如何通过全局变量(vendor_lib_xxxx)间接引用 dll 内模块,以及两个配置如何配合运行与验证。
一、例子定位:先有 vendor dll,后有 app
本仓库在 examples/dll-app-and-vendor/README.md 中给出了一段说明:
This example shows how to use the DllPlugin to separate vendor and app build. This can boost the speed of the app build because vendors are no longer included, but built separately.
整个示例被拆成两个相互独立、按顺序构建的子工程:
- 0-vendor:vendor 侧,负责把第三方依赖(示例中为
example-vendor)单独打包成一个 dll,同时导出一份 manifest; - 1-app:应用侧,也就是本篇文章的核心关联文档所在目录,负责消费上一步的产物。
我们聚焦的 template.md 开头就点明了应用端的全部机制:
The previously built vendor dll is used. The DllReferencePlugin reads the content of the dll from the manifest file and excludes all vendor modules from the compilation. Instead references to these modules will be loaded from the vendor dll via a global variable (
vendor_lib_xxxx).
翻译成三层事实:
- 应用构建时不重新打包 vendor,而是直接使用此前已构建好的 dll;
DllReferencePlugin从 manifest 中读出 dll 的模块登记信息,并把所有 vendor 模块从本次 compilation 中排除;- 应用代码中对这些模块的引用,最终被替换为从一个全局变量(示例命名
vendor_lib_xxxx)中按模块 id 取用。
这正是“应用构建变快”的根源:vendor 代码量大但几乎不变,只有在 vendor 依赖列表变化时才需要重新构建(详见 0-vendor/template.md),正常开发周期内应用侧每次编译都不再触碰它们。
二、应用端 webpack.config.js 逐行拆解
应用侧完整配置位于 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;关键配置说明
| 配置项 | 本例取值 | 作用 |
|---|---|---|
context | __dirname | 模块解析的基准目录,保证./example-app指到应用侧自身的源码 |
entry | "./example-app" | 应用入口(对应 example-app.js) |
output.filename/output.path | app.js/dist | 应用 bundle 输出到dist/app.js |
plugins[0] | DllReferencePlugin | 应用端接入 dll 的唯一入口 |
manifest | require("../0-vendor/dist/vendor-manifest.json") | 以对象字面量直接传入 vendor 侧产出的 manifest |
注意这里manifest与new webpack.DllPlugin({...})一样,是挂在webpack命名空间上的内置插件,因此无需额外安装任何包。
manifest 的两种传法
从源码看,DllReferencePlugin的manifest选项既支持对象,也支持文件路径字符串。lib/dll/DllReferencePlugin.js 的beforeCompile钩子中做了区分处理:
- 当
manifest是字符串时,插件通过compiler.inputFileSystem.readFile读取该文件,再用parseJson解析成 manifest 对象(lib/util/parseJson.js 是仓库内部的 JSON 解析工具); - 若 manifest 文件为空或格式错误,插件不会直接杀死进程,而是把错误暂存起来,稍后作为compilation error抛出(源码中为此专门定义了
DllManifestError),从而保留一次可读的构建失败信息; - 解析成功的 manifest 还会被加入
compilation.fileDependencies,保证 watch 模式下 manifest 变更能触发重新编译。
示例中require(manifest)相当于在 Node 侧直接同步载入 JSON,属于对象传法,二者效果等价。选用哪一种取决于你希望配置是“可读路径”还是“已解析对象”。
name / sourceType / content 的自动推断
如果应用端只传了manifest而没有显式传name,插件在compile钩子中会从 manifest 自动补齐默认值(见 lib/dll/DllReferencePlugin.js):
if (!name) name = manifest.name; if (!sourceType) sourceType = manifest.type; if (!resolvedContent) resolvedContent = manifest.content;也就是说,vendor 侧在 manifest 里写明的name(即全局库名)、type(全局暴露方式)、content(模块名到内部 id 的映射)会被应用端直接沿用。这也是为什么两个工程必须保持“同一套 manifest 契约”。
此外,插件还在validate钩子里对 options 做 schema 校验(lib/dll/DllReferencePlugin.js),对应的校验描述文件位于 schemas/plugins/dll/DllReferencePlugin.json。若你传入的context、scope、type、extensions等可选项拼写错误或类型不符,构建会提前给出 schema 报错而不是运行时才暴露问题。
三、应用源码:import 一个“不参与打包”的 vendor 模块
应用入口 example-app.js 非常简短:
import { square } from "example-vendor"; console.log(square(7)); console.log(new square(7));example-vendor是 vendor 侧的演示依赖模块(源码位于 examples 体系下的 node_modules 中,0-vendor/template.md 中通过../node_modules/example-vendor.js引用它)。它对外导出了square:
square(7)以函数方式调用,输出49之类的计算结果;new square(7)以构造器方式调用,说明这个导出同时具备可调用/可实例化的形态(对应用侧而言只是“从全局 dll 里取到的对象”,具体形态无关紧要)。
值得强调的是:这段import不会触发 vendor 模块进入应用侧的模块图。因为 manifest 里已经声明了example-vendor及其内部模块 id,DllReferencePlugin在compile阶段就把它们映射成了对外部全局变量的引用(详见下文“源码纵深”一节),因此应用 compilation 中既不会解析、也不会打包example-vendor的任何源码——这正是应用构建提速的直接原因。
四、HTML 中的加载顺序:先 vendor,后 app
example.html 展示了 dll 模式下的页面引用方式:
<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>加载顺序是硬约束:
- 浏览器必须先执行
vendor.js,把内部模块加载函数挂到全局变量(vendor_lib_[fullhash])上; - 之后执行
app.js,应用代码运行时再通过该全局变量按 id 取用 dll 内模块。
若顺序颠倒,app.js执行到引用 vendor 模块的语句时会因全局变量尚未定义而报错。此 HTML 是仓库内的演示页模板,实际脚本目录以你本地构建产物位置为准(把 webpack.config.js 跑起来即可得到可用的dist/app.js)。
五、产物与两种构建模式:dist/app.js 里“看不到” vendor 代码
template.md 后续章节列出的dist/app.js、stdout(Unoptimized)与production:stdout(Production mode)均为示例构建时由文档生成工具注入的真实产物与构建日志(模板中的{{_..._}}占位符由 examples 体系下的模板工具替换,这些生成物未提交到仓库)。
你可以这样理解它们的含义:
- dist/app.js:只包含应用自身的业务代码 + 一批“代理模块”(delegated module)。这些代理不携带 vendor 实现,只负责在运行时调用全局 dll 完成模块解析。相比把整个 vendor 打进来,体积显著更小。
- Unoptimized(development)stdout:开发模式下的构建统计,侧重构建耗时与模块数量;
- Production mode stdout:生产模式下的构建统计,此时 webpack 会开启压缩、tree shaking 等优化(示例配置里
mode被注释掉,运行时通过--mode或mode字段切换)。
在仓库测试体系中,test/Examples.test.js 会在 development 与 production 两种模式下分别编译 examples 并断言产物与输出,因此你可以直接以它为准验证 dll 示例在两种模式下的行为。
六、源码纵深:DllReferencePlugin 到底把“引用”替换成了什么
应用端之所以能“只引用不打包”,关键在于 lib/dll/DllReferencePlugin.js 在compile钩子中做的两件事:
/** @type {Externals} */ const externals = {}; const source = `dll-reference ${name}`; externals[source] = name; const normalModuleFactory = params.normalModuleFactory; new ExternalModuleFactoryPlugin(sourceType || "var", externals).apply( normalModuleFactory ); new DelegatedModuleFactoryPlugin({ source, type: this.options.type, scope: this.options.scope, context: this.options.context || compiler.context, content: resolvedContent, extensions: this.options.extensions, associatedObjectForCache: compiler.root }).apply(normalModuleFactory);- 构造 external 映射:插件生成一个特殊的 source 标识
dll-reference <name>,并把<name>映射为要访问的全局变量名。随后用ExternalModuleFactoryPlugin把它注册为“外部模块”,sourceType默认为"var",即最终代码按var全局方式取用。 - 委托模块工厂:
DelegatedModuleFactoryPlugin拿到 manifest 中的content(模块名 → 内部 id 的映射),结合scope、type、extensions等选项,把凡是能命中 content 的模块请求改造成 DelegatedModule。这些委托模块不会携带任何实现代码,只登记“从哪个全局变量、取哪个 id”。 - 模块图的连接方式:插件在
compilation钩子中把DelegatedSourceDependency这类“委托来源依赖”注册到normalModuleFactory(见 lib/dll/DllReferencePlugin.js),从而把引用关系以依赖边的形式纳入模块图,但绝不参与源码打包。
落到运行时,可以概括为:应用 bundle 中引用example-vendor的地方,最终会在全局对象vendor_lib_[fullhash]上按 vendor 侧登记的内部模块 id 取出模块并执行。由于 manifest 中的name写的是vendor_lib_[fullhash],构建后的实际全局名会带上 vendor 构建的哈希后缀——这正是 0-vendor 配置中output.library与DllPlugin.name同时使用[fullhash]的原因:vendor 内容一旦变化,全局名随之变化,应用侧引用随之失效,从机制上规避了缓存串扰。想要深入代理模块的运行时行为,可以继续阅读 lib/dll/DelegatedModule.js、lib/dll/DelegatedModuleFactoryPlugin.js 与 lib/dll/DelegatedSourceDependency.js。
七、vendor 侧快速回顾:这套引用关系从哪来
虽然本文主题是应用端,但要让例子真正跑通,必须先构建 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;对照 0-vendor/template.md 的说明,vendor 侧解决三个问题:
- 把 dll 暴露为全局:
DllPlugin配合output.library,把 dll 内部的模块加载函数暴露为目标环境的全局变量; - 产出 manifest:
DllPlugin.path指定 manifest 写到dist/vendor-manifest.json,其中登记“模块名 → 内部 id”的映射,供应用端DllReferencePlugin读取; - 契约一致性:
DllPlugin.name必须与output.library完全一致(都含[fullhash]),保证 manifest 里记录的name就是真实全局名,应用端才能按名引用。
从 lib/dll/DllPlugin.js 的实现看,DllPlugin内部组合了三块能力:DllEntryPlugin负责把 vendor 入口标记成 dll 入口;LibManifestPlugin负责序列化并写出 manifest(options 会连同entryOnly一起透传);当entryOnly: false时还会通过FlagAllModulesAsUsedPlugin阻止 vendor 内部模块被当作未使用代码裁掉。示例采用默认entryOnly(true),只登记入口能触达的模块映射。
八、端到端运行与验证步骤
由于两个子工程都在仓库 examples 目录内、且webpack通过require("../../../")引用仓库自身,建议使用仓库构建好的 webpack 或 webpack-cli 依次编译:
# 1) 先构建 vendor,产出 dist/vendor.js 与 dist/vendor-manifest.json cd examples/dll-app-and-vendor/0-vendor && webpack # 2) 再构建应用,产出 dist/app.js cd ../1-app && webpack验证要点:
- 确认 1-app/webpack.config.js 中引用的
../0-vendor/dist/vendor-manifest.json已存在且包含name、content字段; - 应用构建日志中的模块数量应远小于 vendor 全量重打的模块数,体现“排除 vendor 模块”的效果;
- 打开页面时,先加载 vendor.js 再加载 app.js,控制台可观察到
square(7)的正常输出,且 Network 面板中不存在对 vendor 源码的二次请求。
九、参考文件清单
| 用途 | 路径 |
|---|---|
| 示例总览(含背景与构建动机) | examples/dll-app-and-vendor/README.md |
| 应用侧模板文档(本文主文档) | examples/dll-app-and-vendor/1-app/template.md |
| 应用侧配置 | examples/dll-app-and-vendor/1-app/webpack.config.js |
| 应用侧入口源码 | examples/dll-app-and-vendor/1-app/example-app.js |
| 应用侧 HTML | examples/dll-app-and-vendor/1-app/example.html |
| vendor 侧配置 | examples/dll-app-and-vendor/0-vendor/webpack.config.js |
| DllReferencePlugin 实现 | lib/dll/DllReferencePlugin.js |
| DllPlugin 实现 | lib/dll/DllPlugin.js |
| 委托模块运行时实现 | lib/dll/DelegatedModule.js |
| dll 模块解析与工厂 | lib/dll/DllModuleFactory.js、lib/dll/DelegatedModuleFactoryPlugin.js |
| 示例自动化测试(双模式验证) | test/Examples.test.js |
【免费下载链接】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),仅供参考