遇见Tapable
Webpack的成功之处不仅在于它强大的打包构建能力,也在于它灵活的插件机制。
大大的疑问?🤔️
- webpack插件配置是有序的吗?
- 写插件的时候,都要写成类的形式吗?
- 插件原型为什么一定要有apply方法?
Webpack内部拥有一套自己的事件流机制,它的工作流程是将各个插件串联起来,而实现这个串联的核心就是Tapable。
1. Webpack插件机制
1.1 Webpack插件被插入的时机
当定义了Webpack配置文件时,webpack开始工作Compiler对象会被实例话,而且是全局唯一的,Compiler包含了当前运行的webpack的配置,而插件就是在实例化Compiler对象时被添加到webpack的运行流程中的,看下源码(定位:lib/webpack.js:61):
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
// 判断options.plugins插件配置是否存在
if (Array.isArray(options.plugins)) {
// 遍历插件
for (const plugin of options.plugins) {
// 这里有两种编写插件的方式,一种function,一种是对象实例
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
// 如果是对象实例,则需要在构造函数中定义一个apply方法,作为webpack调用该插件的入口。然后将compiler对象作为参数传递。
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
从遍历options.plugins这里开始看,不难看出调用插件通过两种方式:
- 插件可以是一个函数,函数的作用域是当前的compiler,同时compiler也会作为参数传递。
- 对象实例,并且原型链上有apply方法,调用apply,并将当前compiler传入。 所以,这也就解释了为什么插件需要使用new,并且插件中都要定义apply方法。
1.2 在Compiler中做了什么?
从上面可以看出,plugin在注入时传入了当前被实例化出来的compiler实例,所以看一下compiler中做了什么?(源码定位:lib/Compiler.js:117)
class Compiler {
constructor(context) {
// Object.freeze 冻结对象,不能被修改、删除、添加,包括原型也不可修改!
this.hooks = Object.freeze({
// ...
/** @type {SyncBailHook<[Compilation], boolean>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {SyncHook<[CompilationParams]>} */
compile: new SyncHook(["params"]),
/** @type {AsyncParallelHook<[Compilation]>} */
make: new AsyncParallelHook(["compilation"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
emit: new AsyncSeriesHook(["compilation"])
// ...
})
}
emitAssets(compilation, callback) {
let outputPath;
// 如果有插件中监听了emit钩子,这里将会被触发。
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath, {});
mkdirp(this.outputFileSystem, outputPath, emitFiles);
});
}
}
通过上述源码可以看出,每一个compiler实例都会包含很多个钩子,webpack正式依赖这些hook完成了代码的构建,并且在编写plugin时,可以利用不同的钩子完成很多特殊的定制化的需求。
2. Tapable
Tapable的核心思路类似于node.js的EventEmitter,最基本的发布/订阅模式。
const events = require('events');
const emitter = new events.EventEmitter();
// 注册事件监听和对应的回调函数
emitter.on('demo', params => {
console.log('输入结果', params);
})
// 触发事件,传入参数
emitter.emit('demo', '遇见Tapable');
3. Tapable的hook介绍
tapable有如下10个Hook:
如何使用
先从最简单的开始,钩子都是大同小异,懂一个就其余的就很好懂了。
3.1 各个Hook的用法
- Basic: 不关心回调函数返回值。SyncHook, AsyncParallelHook, AsyncSeriesHook
- Bail: 只要其中一个监听函数的返回值不为undefined,则终止执行。SyncBailHook, AsyncParallelBailHook, AsyncSeriesBailHook
- Waterfall: 前一个监听函数的返回值不为undefined,则作为下一个监听函数的第一个参数。SyncWaterfallHook, AsyncSeriesWaterfallHook
- Loop: 如果有一个监听函数的返回值不为undefined,则终止向下执行,从头开始执行,直到所有监听函数的返回值均为undefined。SyncLoopHook, AsyncSeriesLoopHook
3.2 SyncHook
const { SyncHook } = require('tapable');
let hook = new SyncHook(['name']);
hook.tap('demo', function(params) {
console.log('demo', params);
});
hook.tap('demo2', function(params) {
console.log('demo2', params);
return true;
});
hook.tap('demo3', function(params) {
console.log('demo3', params);
});
hook.call('hello SyncHook');
输出/*
* demo hello SyncHook
* demo2 hello SyncHook
* demo3 hello SyncHook
*/
根据上述使用实例,简单写下其实现原理:
class SyncHook {
constructor(args = []) {
this._args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call(...args) {
const params = args.slice(0, this._args.length);
this.tasks.forEach(task => task(...params))
}
}
3.3 SyncBailHook
const { SyncBailHook } = require('tapable');
let hook = new SyncBailHook(['name']);
hook.tap('demo', function(params) {
console.log('demo', params);
})
hook.tap('demo2', function(params) {
console.log('demo2', params);
return true;
})
hook.tap('demo3', function(params) {
console.log('demo3', params);
})
hook.call('hello SyncBailHook');
输出/*
* demo hello SyncBailHook
* demo2 hello SyncBailHook
*/
根据上述输入结果显示,SyncBailHook钩子遇到回调函数中返回结果不为undefined时,则跳过执行下面所有逻辑。实现原理如下:
class SyncBailHook {
constructor(args) {
this._args = args;
this.tasks = [];
}
tap(name, task) {
this.tasks.push(task);
}
call() {
const args = Array.from(arguments).slice(0, this._args.length);
for (let i = 0; i < this.tasks.length; i++) {
const result = this.tasks[i](...args);
if (result !== undefined) break;
}
}
}
3.4 总结
通过上述两个钩子,可以发现tapable提供了各式各样的Hook来管理事件如何执行。tapable的核心功能就是控制一系列注册事件之间的执行流,比如注册了三个事件,不论是异步并发(串行)执行的,还是同步依次执行,还是通过回调函数返回值控制执行流,都可以通过tapable提供的Hook一一实现。
4.应用实例
4.1 plugin举例
举一个最简单的例子,我们经常用到的copyWebpackPlugin,看下源码中是如何写的(源码定位:src/index.js:607)
class CopyPlugin {
// ...
apply(compiler) {
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
// ...
})
}
// ...
}
CopyPlugin中有一个apply方法,然后监听了compiler上thisCompilation这个钩子,从而当webpack中执行这个钩子的call方法时,就会触发此处的回调函数。
5. 强行吐槽
- 监听器一旦添加无法移除,官方团队认为tapable是为静态插件服务的,移除监听器与设计理念不符,所以这点很大程度限制了tapable的拓展。
- 同/异步钩子可以混用,着实有点混乱。
那么到这里,就分享结束了,你~学废了吗?点个赞再走呗~