如果你想学到更多实用前端知识。
可以关注我的公众号:【前端驿站Lite】,一个不止分享前端的地方 ᕦ( •̀∀•́)ᕤ
阅读收获
阅读完本篇文章,你将会有以下收获:
- webpack为什么会有plugin机制。
- webpack运行原理是怎样的。
- 详细分析了compiler和compilation的区别。
- Tapable是什么?有哪些钩子?如何使用?
- 如何自定义一个plugin。
- 3个自定义plugin实战案例。
- 9个常用plugin介绍与用法。
plugin机制出现原因
前面我们已经知道了,loader机制让webpack拥有了处理除js
类型文件以外的能力。
那如果我们需要在项目中实现打包前自动清理上次打包生成的文件
、将一些文件复制到打包目录中
、自动生成html文件
、将打包产物自动上传至服务器
、将打包后代码进行压缩、拆分
等一系列定制化功能,此时就必须借助webpack的plugin机制去实现了。
没错,webpack的plugin机制让webpack有了定制化的能力。
plugin原理
那具体如何通过plugin机制去实现这些定制化功能呢?
其实是webpack在打包过程中的不同阶段(配置文件读取完成后、打包开始前、打包完成后等阶段)会触发不同的钩子,我们只需要明确要实现的功能应该在哪个阶段,然后将具体实现代码注册为对应钩子的事件即可。
webpack运行原理
我们在了解这些钩子之前,必须要知道webpack的运行原理。
这是一个简化版的webpack打包过程,当我们执行webpack build
命令后,webpack会先读取配置文件,然后根据配置文件中的配置项去初始化,创建一个compiler
对象,然后调用compiler
对象的run
方法,初始化一个compilation
对象,执行compilation
中的build
方法进行编译,编译完成后,触发compiler
对象的done
钩子,完成打包。
//第一步:搭建结构,读取配置参数,这里接受的是webpack.config.js中的参数
function webpack(webpackOptions) {
//第二步:用配置参数对象初始化 `Compiler` 对象
const compiler = new Compiler(webpackOptions);
//第三步:挂载配置文件中的插件
const {
plugins } = webpackOptions;
for (let plugin of plugins) {
plugin.apply(compiler);
}
return compiler;
}
//Compiler其实是一个类,它是整个编译过程的大管家,而且是单例模式
class Compiler {
constructor(webpackOptions) {
//省略
}
// 第五步:创建compilation对象
compile(callback){
//虽然webpack只有一个Compiler,但是每次编译都会产出一个新的Compilation,
//这里主要是为了考虑到watch模式,它会在启动时先编译一次,然后监听文件变化,如果发生变化会重新开始编译
//每次编译都会产出一个新的Compilation,代表每次的编译结果
let compilation = new Compilation(this.options);
compilation.build(callback); //执行compilation的build方法进行编译,编译成功之后执行回调
}
//第四步:执行`Compiler`对象的`run`方法开始执行编译
run(callback) {
this.hooks.run.call(); //在编译前触发run钩子执行,表示开始启动编译了
const onCompiled = () => {
// 第七步:当编译成功后会触发done这个钩子执行
this.hooks.done.call();
};
this.compile(onCompiled); //开始编译,成功之后调用onCompiled
}
}
class Compilation {
constructor(webpackOptions) {
this.options = webpackOptions;
this.modules = []; //本次编译所有生成出来的模块
this.chunks = []; //本次编译产出的所有代码块,入口模块和依赖的模块打包在一起为代码块
this.assets = {
}; //本次编译产出的资源文件
this.fileDependencies = []; //本次打包涉及到的文件,这里主要是为了实现watch模式下监听文件的变化,文件发生变化后会重新编译
}
//第六步:执行compilation的build方法进行编译
build(callback) {
//这里开始做编译工作,编译成功执行callback
// ... 编译过程代码省略
// 编译完成后,触发callback回调
callback()
}
}
compiler 与 compilation
那上面提到的compiler
对象和compilation
对象到底是什么呢?又有什么区别与联系?
compiler
对象包含了webpack的所有配置信息,包括entry
、output
、module
、plugins
等,compiler
对象会在启动webpack时,一次性地初始化创建,它是全局唯一的,可以简单理解为webpack的实例。compilation
对象代表一次资源的构建,通过一系列API可以访问/修改本次模块资源、编译生成的资源、变化的文件、以及被跟踪依赖的状态信息等,当我们以开发模式运行webpack时,每当检测到一个文件变化,就会创建一个新的compilation
对象,所以compilation
对象也是一次性的,只能用于当前的编译。
他有以下主要属性:
compilation.modules
解析后的所有模块compilation.chunks
所有的代码分块chunkcompilation.assets
本次打包生成的所有文件compilation.hooks
compilation所有的钩子
所以说呢,compiler
代表的是整个 webpack 从启动到关闭的生命周期(终端结束,该生命周期结束), 而 compilation
只是代表了一次性的编译过程,如果是watch
模式,每次监听到文件变化,都会产生一个新的 compilation,所以 compilation 代表一次资源的构建,会多次被创建,而 compiler 只会被创建一次。
我们了解了compiler
和compilation
对象后,就可以来看一下到底有哪些钩子。
compiler钩子
compiler
有很多钩子官方地址、中文地址,介绍几个常用的:
beforeRun
AsyncSeriesHook类型,开始读取配置文件前触发。run
AsyncSeriesHook类型,开始编译后触发。watchRun
,AsyncSeriesHook类型,在监听模式下,一个新的 compilation 触发之后,但在 compilation 实际开始之前触发。compile
SyncHook类型,一次新的编译(compilation)创建之前触发。compilation
SyncHook类型,一次新的编译(compilation)创建完成后触发。emit
AsyncSeriesHook类型,生成资源到 output 目录之前触发。done
AsyncSeriesHook类型,compilation编译完成后触发。failed
SyncHook类型,compilation编译失败后触发。
compilation钩子
compilation
对象也有很多钩子官方地址、中文地址,介绍几个常用的:
buildModule
SyncHook类型,模块开始编译前,执行该钩子,可以用于修改模块内容。succeedModule
SyncHook类型,模块编译成功后,执行该钩子。finishModules
AsyncSeriesHook类型,所有模块编译完成后,执行该钩子。seal
SyncHook类型,在构建过程封存前触发,允许在最终资源生成之前进行一些操作。optimize
SyncHook类型,资源优化前触发,可以用于自定义资源优化逻辑。optimizeAssets
AsyncSeriesHook类型,在资源优化过程中触发,可以监听和修改资源的优化过程。optimizeChunkAssets
:AsyncSeriesHook类型,在块资源优化过程中触发,可用于自定义块资源的优化逻辑。optimizeTree
:AsyncSeriesHook类型,在资源树优化过程中触发,允许修改资源树的优化逻辑。afterOptimizeTree
:SyncHook类型,在资源树优化完成后触发,可用于处理优化完成后的资源树。beforeHash
:SyncHook类型,在计算输出文件的哈希之前触发,可以监听和修改哈希生成的逻辑。afterHash
:SyncHook类型,在输出文件哈希计算完成后触发,可用于处理生成的哈希值。beforeModuleAssets
:SyncHook类型,在生成模块资源之前触发,可用于在模块资源生成前执行一些操作。moduleAsset
:SyncHook类型,在生成模块资源时触发,可监听和修改模块资源的生成。processAssets
:AsyncSeriesHook类型,在生成资源(如 JavaScript 文件、CSS 文件等)时触发,可以监听和修改资源的生成。
每个钩子都有对应的类型,那这些类型有什么区别呢?
接下来,我们需要了解下Tapable
Tapable是什么
Tapable是一个提供事件发布订阅的工具,通过其提供的一系列钩子,我们可以注册事件,然后在不同的阶段去触发这些注册的事件。
webpack的plugin机制正是基于 Tapable 实现的,在不同编译阶段触发不同的钩子。
Tapable 官方文档提供了这九种钩子,也就是我们上面提到的钩子类型:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
可以看到,这些钩子有两种开头,分别是Sync和Async,这两种钩子的区别是:
Sync
开头的为同步钩子,表示注册的事件函数会同步进行执行Async
开头的为异步钩子,表示注册的事件函数会异步进行执行
同时呢,这些钩子还有三种结尾,分别是Hook
、BailHook
、WaterfallHook
、LoopHook
,这三种结尾的区别是:
Hook
结尾的为普通钩子,只会按顺序挨个执行注册的事件,不会去管事件函数的返回值是什么。
BailHook
结尾的为保险钩子,只要注册的事件函数有一个返回值不为undefined
,就会停止执行后面的事件函数。
WaterfallHook
结尾的为瀑布钩子,注册的事件函数会按顺序执行,每个事件函数的返回值会作为下一个事件函数的参数,只会影响下一个事件函数的第一个参数。
LoopHook
结尾的为循环钩子,注册的事件函数会按顺序执行,只要执行的事件返回值非undefeind,就会立即重头开始执行,直到所有的事件函数都返回undefined,这个钩子才会结束。
接下来,我们又发现,异步钩子又是以AsyncParallel
、AsyncSeries
开头,这又有什么区别呢?
AsyncSeries
为异步串行钩子,注册的事件函数会按顺序挨个执行,每个事件函数执行完后,会调用回调函数,然后再执行下一个事件函数。AsyncParallel
为异步并行钩子,注册的事件函数会同时执行,不会等待上一个事件函数执行完毕后再执行下一个事件函数。
下面我们就来讲一下这些钩子如何去使用。
Tapable同步钩子
同步钩子只需要调用tap
方法注册事件,然后调用call
方法触发事件即可。
1. SyncHook
SyncHook 是一个同步的、普通类型的 Hook,注册的事件函数会按顺序挨个执行,不会去管事件函数的返回值是什么。
const {
SyncHook } = require('tapable');
// 初始化钩子,定义形参
const hook = new SyncHook(['name', 'age']);
// 注册事件1
hook.tap('事件1', (name, age) => {
console.log('事件1执行:', name, age);
});
// 注册事件2
hook.tap('事件2', (name, age) => {
console.log('事件2执行:', name, age);
});
// 触发事件,传入实参
hook.call('前端', 18);
// 执行结果
// 事件1执行: 前端 18
// 事件2执行: 前端 18
2. SyncBailHook
SyncBailHook 是一个同步的、保险类型的 Hook,意思是只要其中一个有返回了,后面的就不执行了。
const {
SyncBailHook } = require('tapable');
// 初始化钩子,定义形参
const hook = new SyncBailHook(['name', 'age']);
// 注册事件1
hook.tap('事件1', (name, age) => {
console.log('事件1执行:', name, age);
});
// 注册事件2
hook.tap('事件2', (name, age) => {
console.log('事件2执行:', name, age);
return 'abc'
});
// 注册事件3
hook.tap('事件3', (name, age) => {
console.log('事件3执行:', name, age);
}<