Webpack 插件深度剖析
一、插件初印象
(一)插件是什么
插件在 Webpack 里可是个“神奇工具”,它既可以是一个 class
(就像是一个精心打造的工具箱,里面装着各种解决问题的工具方法),也可以是一个函数(如同一个便捷的小工具,能快速完成单一任务)。
(二)关键的 apply 方法
每个插件的原型对象上都得有个 apply
方法,这就好比是插件与 Webpack 沟通的“桥梁”。当 Webpack 启动创建 compiler
对象时,就会依次调用各个插件实例上的 apply
方法,还会贴心地把 compiler
对象当作“见面礼”传进去,方便插件后续大展身手。
来看看下面这个 DonePlugin
例子:
class DonePlugin {
apply(compiler) {
// 注册了 Compiler Hook,并添加了自己的逻辑
compiler.hooks.done.tap('Plugin Done', () => {
console.log('compilation done');
});
}
}
module.exports = DonePlugin;
这个 DonePlugin
插件,通过 apply
方法在 compiler
的 done
钩子上绑定了自己的逻辑,一旦 Webpack 编译完成,就会在控制台打印出
compilation done
,就像完成一项大工程后放个庆祝烟花一样。
二、Hook 的巧妙运用
(一)Hook 绑定
要想让插件发挥作用,得先给它找个“立足之地”,也就是指定一个绑定在 compiler
对象上的 Hook,这就好比给风筝找个合适的挂钩,才能让它在天空翱翔。
(二)回调处理
在找到的 Hook 回调里,就是插件施展“魔法”的舞台,在这里处理插件自身独特的逻辑,按照自己的规则对 Webpack 的流程进行微调。
(三)通知 Webpack
不同的 Hook 有不同的“性格”,在插件完成自己的逻辑后,得根据 Hook 的特性,礼貌地通知 Webpack 继续推进后续流程,就像接力赛中交接棒一样顺畅,不然整个构建过程就“卡壳”啦。
三、常用对象大揭秘
(一)compiler 对象——Webpack 的“总指挥”
当 Webpack 首次启动,就会诞生一个 compiler
对象,它可是掌控全局的“总指挥”,通过它,就能深入 Webpack 的主环境配置核心地带。
- 属性宝藏:
compiler.options
:这就像是一本“秘籍”,记录着编译过程中 Webpack 的完整配置信息,不管是优化策略还是模块路径,统统都能查到。compiler.inputFileSystem
:如同一个勤劳的“小书童”,提供读文件的 API,帮你快速获取项目里的各种资料。compiler.outputFileSystem
:是个“写作小能手”,带着写文件的 API,负责把编译好的成果妥善保存。compiler.hooks
:这是一串神奇的“铃铛”,每个铃铛(钩子)都代表一个关键节点,像initialize
是起跑的发令枪,beforeRun
是赛前热身,run
正式开跑,beforeCompile
准备编译资料,compile
埋头苦干编译,done
大功告成庆祝,afterDone
打扫战场复盘,每个阶段都能精准干预。
(二)compilation 对象——资源构建的“加工厂”
compilation
对象负责一次资源的构建工作,在这个“加工厂”里,可以接触到所有模块以及它们复杂的“亲戚关系”(依赖)。
- 属性清单:
compilation.modules
:想象成一个“零件盒”,里面装着 Set 类型的模块列表,每个模块都是独特的零件,等待组装。compilation.chunks
:是把多个模块“打包”成的代码块,如同用零件组装成的功能组件,一起协同工作。compilation.assets
:则是本次“生产”打包生成的所有文件成果,是最终出厂的“产品”。
- 方法秘籍:
compilation.hooks
:同样藏着多个钩子,如同工厂里的“质检员”,监听构建过程各个阶段,确保每个环节都符合要求。
(三)ContextModuleFactory Hook——文件目录的“导航仪”
contextModuleFactory
提供的一系列 Hook,就像是给 Webpack 在解析独有的 require.context
文件目录时配备的“导航仪”。当
Webpack 在错综复杂的文件目录里“迷路”时,它能精准指引,找到需要的资源,让文件引入有条不紊。
(四)NormalModuleFactory Hook——模块生成的“调控器”
Webpack
靠着 NormalModuleFactory
模块孕育各类模块,这个过程就像是从一颗种子(入口文件)开始发芽,它会把每个模块请求逐步拆解,深挖文件内容,寻找更多隐藏的“种子”(进一步的请求),然后不断循环,直到所有依赖项都成长为成熟的模块实例。
而我们可以通过 NormalModuleFactoryHook
给这个成长过程注入插件逻辑,就像是给植物生长添加特殊的“营养液”,比如在处理
ESM、CJS 等模块引入前后,按照我们的想法优化调整,实现个性化的模块引用控制。
compiler.hooks.normalModuleFactory.tap(
"MyPlugin",
(normalModuleFactory) => {
normalModuleFactory.hooks.beforeResolve.tap(
"MyPlugin",
(resolveData) => {
console.log("****resolveData.request", resolveData.request);
// 只解析符合条件的文件
return resolveData.request === "./src/index.js";
}
);
}
);
解释
compiler.hooks.normalModuleFactory.tap
:这相当于给NormalModuleFactory
这个“成长工厂”挂上一个专属“名牌”(注册插件),告诉 Webpack 这里有特殊规则。normalModuleFactory.hooks.beforeResolve.tap
:在模块开始“找身份”(解析)之前,偷偷塞进去一段逻辑,改变它的解析走向。resolveData.request
:就是模块此刻正在寻找的“成长路径”(当前解析的模块请求路径),我们可以根据这个判断它是不是走对了路。- 返回值:如果返回
true
,就好比给模块发了一张“通行证”,继续解析;要是返回false
或undefined
,那模块就只能原地待命,跳过解析,确保只有特定条件下的文件才能进入后续流程,达到精细管理的目的。
(五)JavascriptParserHook——AST 节点的“化妆师”
JavascriptParserHook
可是个在模块解析生成 AST
节点时登场的“化妆师”。webpack
用 Parser
给每个模块“卸妆”(解析),露出最本质的结构,这时候插件就可以通过注册 JavascriptParserHook
,在这个关键节点给模块“化妆”,添加额外的逻辑,让模块更加符合项目需求。
const ParserHelpers = require('webpack/lib/javascript/JavascriptParserHelpers')
class DefinePlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
compiler.hooks.normalModuleFactory.tap('DefinePlugin', (factory) => {
factory.hooks.parser.for('javascript/auto').tap('DefinePlugin', (parser) => {
for (const key in this.options) {
parser.hooks.expression.for(key).tap('DefinePlugin', (expression) => {
return ParserHelpers.toConstantDependency(parser, JSON.stringify(this.options[key]))(expression)
})
}
})
})
}
}
module.exports = DefinePlugin
这个 DefinePlugin
插件就是利用 JavascriptParserHook
,在解析 javascript/auto
类型模块时,按照预设的 options
,对特定表达式进行处理,就像是给素颜的模块画上精致的妆容,让它在后续流程中展现独特魅力。