webpack插件开发必会Tapable
tapable
是官方提供的一个核心工具,不仅可以用于wepback
当中,还可以使用在任何你需要的地方,webpack
很多类都扩展自tapable
,所以学习它对于学习webpack
有很大的能帮助,文档在这=>Tapable。
认识Tapable
官方对tapable
的定义是钩子,业界很多写tapable
的觉得它是发布订阅模式,它确实很像发布订阅模式,但是不完全是,至于官方说的钩子,个人感觉在webpack
内部可以称作钩子,因为是官方定义的,所以肯定是按照设计的想法来使用的,如果是你自己使用,操作不当可能就不是钩子了,毕竟只是一个工具,你拿扳手当锤子也没人说你什么,以上是个人见解,下面附上一些验证的思路。
不喜欢论证,喜欢看使用的可以跳过这个小标题,到第二个标题
tapable使用详解
。
1. 发布订阅模式
先来看看什么是发布订阅,发布订阅是需要分为两个部分,一个是发布,一个是订阅。
-
发布
发布者就是我,我写下这篇文章,点击发布就是发布了。
-
订阅
订阅者是你,但是你要先关注我,关注我之后我发布文章就会通知你。
对应转化成代码应该是下面这样的:
执行结果
代码不是正儿八经的发布订阅模式写的,但是思想还是差不多的,主要是为了还原我自己画的图,正儿八经的发布订阅模式有三个模块,一个是发布者,一个是订阅者,一个调度中心,我这里发布者和订阅者合并到一起了,调度中心还分管着权限。
这里主要看使用方式,最典型的就是dom
的events
事件:
const btn = document.getElementById('button');
btn.addEventListener('click', () => {
console.log('订阅点击事件')
});
// 发布点击事件
btn.click();
等会再来讨论这个代码,接下来看看钩子。
2. 钩子
钩子函数听得比较多,主要来源是React
,还有Vue3
的composition api
,钩子函数是一种消息处理机制,本质是用来处理系统消息的,通过应用系统调用分配,将其挂入应用系统,看看百度百科的解释(不需要你看文档,稍微会点百度也可以知道):钩子。
来看上面写的其实就知道钩子是和应用程序挂钩的,是由应用程序提供的,简单的实现一下:
class HooksApp {
hooks = {
'onBeforeCreated': [],
'onCreated': [],
'onBeforeDestroyed': [],
'onDestroyed': []
};
onHooks(hookName, callback) {
if (this.hooks[hookName]) {
this.hooks[hookName].push(callback);
}
}
created() {
this.hooks.onBeforeCreated.forEach((callback) => {
callback();
});
// 创建需要一秒钟
const now = new Date();
while (new Date() - now < 1000) ;
this.hooks.onCreated.forEach((callback) => {
callback();
});
}
update() {
console.log('我有一个update,我不提供钩子!')
}
destroy() {
this.hooks.onBeforeDestroyed.forEach((callback) => {
callback();
});
// 销毁需要一秒钟
const now = new Date();
while (new Date() - now < 1000) ;
this.hooks.onDestroyed.forEach((callback) => {
callback();
});
}
}
const hooksApp = new HooksApp();
// 注册两次 onBeforeCreated 钩子
hooksApp.onHooks('onBeforeCreated', () => {
console.log('onBeforeCreated1');
});
hooksApp.onHooks('onBeforeCreated', () => {
console.log('onBeforeCreated2');
});
// 注册 onCreated 钩子
hooksApp.onHooks('onCreated', () => {
console.log('onCreated3');
});
// 注册 onBeforeDestroyed 钩子
hooksApp.onHooks('onBeforeDestroyed', () => {
console.log('onBeforeDestroyed4');
});
// 注册 onDestroyed 钩子
hooksApp.onHooks('onDestroyed', () => {
console.log('onDestroyed5');
});
hooksApp.created();
hooksApp.update();
hooksApp.destroy();
执行结果
这个代码没写多少注释,而且这个代码也很简单,钩子也比较简单好理解,钩子的数量多寡要看应用程序的开发者提供多少,像我这个就不提供update
的钩子,你就不能干预我update
的流程。
tapable
来看看tapable
的使用,这个就没有什么源码和设计了,直接是使用了。
const { SyncHook } = require("tapable");
// 实例化同步钩子
const syncHook = new SyncHook(["name", "age"]);
// 注册事件
syncHook.tap('abc', (name, age) => {
console.log(name, age);
})
// 注册事件
syncHook.tap('def', (name, age) => {
console.log(name, age);
})
// 注册事件
syncHook.tap('abc', (name, age) => {
console.log(name, age);
})
// 触发事件
syncHook.call('zhangsan', 18);
执行结果
三者的区别
-
使用方式
发布订阅模式
需要先订阅,才能收到订阅消息;钩子
要先注册钩子,才能在触发钩子对应的回调;tapable
需要注册事件才能触发对应的回调;- 代码书写上大致相同,不分伯仲;
-
系统架构
发布订阅模式
主要是依靠调度中心发送通知,需要有一个发布者和订阅者配合使用;钩子
主要是应用系统级的事件回调,需要应用接入者和对应的钩子对接,钩子的个数和种类由应用系统决定;tapable
是一个工具包,将事件处理抽象出来,形成一个单独的模块,这个模块是钩子还是发布订阅先不下结论。
-
使用效果
-
在
发布订阅模式
下,只有订阅者
才能收到发布者
发布的消息; -
在
钩子
下,只有注册了对应的钩子才能触发对应的事件; -
在
tapable
下,主要注册了这个类型的,都可以触发事件;
这个可以参考我上面截图的运行效果,也可以自己运行一下上面的代码对比一下结果。
-
结论
现在我讲一下我目前看到的结果,tapable
肯定不是发布订阅模式,只是长得比较像而已,来看看下面的代码对比
// 发布订阅模式使用
const btn = document.querySelector('button');
btn.addEventListener('click', () => {
console.log('click');
});
btn.addEventListener('dblclick', () => {
console.log('click');
});
btn.click();
// 运行结果:
// click
// tapable使用
const { SyncHook } = require("tapable");
const syncHook = new SyncHook();
syncHook.tap('click', () => {
console.log('click');
});
syncHook.tap('dblclick', () => {
console.log('dblclick');
});
syncHook.call();
// 运行结果:
// click
// dblclick
上面的代码可以说几乎是一样的了,但是运行结果却是截然不同,所以肯定不是发布订阅模式了,网上的博文是是发布订阅模式不知道从哪看的。
再来看看钩子,其实和钩子很像,使用方式很像,架构也很像,但是它可以和钩子一样,也可以不一样,具体就要看怎么使用的了,钩子就拿我上面代码写的示例:
const hooksApp = new HooksApp();
// 注册两次 onBeforeCreated 钩子
hooksApp.onHooks('onBeforeCreated', () => {
console.log('onBeforeCreated1');
});
hooksApp.onHooks('onBeforeCreated', () => {
console.log('onBeforeCreated2');
});
// 注册 onCreated 钩子
hooksApp.onHooks('onCreated', () => {
console.log('onCreated3');
});
// 注册 onBeforeDestroyed 钩子
hooksApp.onHooks('onBeforeDestroyed', () => {
console.log('onBeforeDestroyed4');
});
// 注册 onDestroyed 钩子
hooksApp.onHooks('onDestroyed', () => {
console.log('onDestroyed5');
});
// 这里需要调用两个方法
hooksApp.created();
hooksApp.destroy();
// 运行结果:
// onBeforeCreated1
// onBeforeCreated2
// onCreated3
// onBeforeDestroyed4
// onDestroyed5
// tapable使用
const { SyncHook } = require("tapable");
const syncHook = new SyncHook();
syncHook.tap('onBeforeCreated', () => {
console.log('onBeforeCreated1');
});
syncHook.tap('onBeforeCreated', () => {
console.log('onBeforeCreated2');
});
syncHook.tap('onCreated', () => {
console.log('onCreated');
});
syncHook.tap('onBeforeDestroyed', () => {
console.log('onBeforeDestroyed');
});
syncHook.tap('onDestroyed', () => {
console.log('onDestroyed');
});
// 这里只调用了一次
syncHook.call();
// 运行结果:
// onBeforeCreated1
// onBeforeCreated2
// onCreated
// onBeforeDestroyed
// onDestroyed
使用钩子如果要运行完所有的注册的钩子事件,需要将应用的整个涉及到钩子的流程都执行一遍,而tapable
只需要调用一次call
方法就可以了,而且tapable
一次调用,所有的注册的事件全部执行,没有流程之分。
那么如果需要tapable
实现和钩子一样的效果怎么办?那就创建多个实例呗,所以这也是我说为什么也不完全是钩子的原因,现在还是用我上面钩子的示例,使用tapable
改造一下:
const {SyncHook} = require('tapable');
class HooksApp {
hooks = {
'onBeforeCreated': new SyncHook(),
'onCreated': new SyncHook(),
'onBeforeDestroyed': new SyncHook(),
'onDestroyed': new SyncHook()
};
created() {
this.hooks.onBeforeCreated.call();
// 创建需要一秒钟
const now = new Date();
while (new Date() - now < 1000) ;
this.hooks.onCreated.call();
}
update() {
console.log('我有一个update,我不提供钩子!')
}
destroy() {
this.hooks.onBeforeDestroyed.call();
// 销毁需要一秒钟
const now = new Date();
while (new Date() - now < 1000) ;
this.hooks.onDestroyed.call();
}
}
const hooksApp = new HooksApp();
hooksApp.hooks.onBeforeCreated.tap('onBeforeCreated', () => {
console.log('onBeforeCreated')
});
hooksApp.hooks.onCreated.tap('onCreated', () => {
console.log('onCreated')
});
hooksApp.hooks.onBeforeDestroyed.tap('onBeforeDestroyed', () => {
console.log('onBeforeDestroyed')
});
hooksApp.hooks.onDestroyed.tap('onDestroyed', () => {
console.log('onDestroyed')
});
hooksApp.created();
hooksApp.destroy();
// 运行结果:
// onBeforeCreated
// onCreated
// onBeforeDestroyed
// onDestroyed
到这里,不知道大家有没有一个疑惑,就是使用tapable
时,前面的字符串应该传什么好?我也有这个疑问,我还找过资料,看了SyncHooks
的源码,发现这个玩意没有被使用到,这一块就留个疑问,毕竟自己也是刚学,应该是很重要的,在源码中是必填的。
tapable使用详解
上面的论证的案例里使用的是最简单的SyncHooks
,下面是完整的列表:
名称 | 解释 |
---|---|
SyncHook | 同步钩子 |
SyncBailHook | 同步熔断钩子 |
SyncWaterfallHook | 同步瀑布流钩子 |
SyncLoopHook | 同步循环钩子 |
AsyncParallelHook | 异步并行钩子 |
AsyncParallelBailHook | 异步并行熔断钩子 |
AsyncSeriesHook | 异步串行钩子 |
AsyncSeriesBailHook | 异步串行熔断钩子 |
AsyncSeriesLoopHook | 异步串行循环钩子 |
AsyncSeriesWaterfallHook | 异步串行瀑布流钩子 |
SyncHook
const {SyncHook} = require('tapable');
// 实例化时可以传入参数,参数为数组,数组中的每一项为字符串,表示参数的名称
const syncHook = new SyncHook(['name', 'age']);
syncHook.tap('1', (name, age) => {
console.log(1, name, age);
});
console.log('开始执行')
syncHook.call('panda', 18);
console.log('执行第一个结束');
syncHook.call('monkey', 80);
console.log('执行第二个结束');
// 执行结果:
// 开始执行
// 1 panda 18
// 执行第一个结束
// 1 monkey 80
// 执行第二个结束
同步钩子是最简单最好理解的,这个就不过多解读了,看的再多不如自己亲手尝试一下。
SyncBailHook
const {SyncBailHook} = require('tapable');
// 同步熔断钩子,当某个监听函数返回值不为undefined时,后续的监听函数不再执行
const syncBailHook = new SyncBailHook();
syncBailHook.tap('1', () => {
console.log(1);
});
syncBailHook.tap('2', () => {
console.log(2);
// 返回值非 undefined 时,后续的监听函数不再执行
return 2;
});
syncBailHook.tap('3', () => {
console.log('这个不会执行');
});
syncBailHook.call();
// 执行结果:
// 1
// 2
同步熔断钩子,和SyncHook
使用方式相同,不同的是如果返回值非undefined
时,后面注册的监听函数都不会再执行。
SyncWaterfallHook
const {SyncWaterfallHook} = require('tapable');
// 同步串行钩子,监听函数的返回值会作为参数传递给下一个监听函数
const syncWaterfallHook = new SyncWaterfallHook(['name', 'age']);
syncWaterfallHook.tap('1', (name, age) => {
console.log(1, name, age);
return {
name: 'panda',
age: 18
}
})
syncWaterfallHook.tap('2', (data) => {
console.log(2, data.name, data.age);
return {
name: 'monkey',
age: 80
}
});
syncWaterfallHook.tap('3', (data) => {
console.log(3, data.name, data.age);
});
syncWaterfallHook.call('cat', 8);
// 执行结果:
// 1 cat 8
// 2 panda 18
// 3 monkey 80
同步串行钩子,和SyncHook
使用方式相同,不同的是监听函数的返回值会作为参数传递给下一个监听函数。
SyncLoopHook
const {SyncLoopHook} = require('tapable');
// 同步循环钩子,当监听函数返回值为true时,会重复执行监听函数
const syncLoopHook = new SyncLoopHook(['name', 'age']);
let count = 0;
syncLoopHook.tap('1', (name, age) => {
console.log(1, name, age);
return ++count === 3 ? undefined : '继续执行';
});
syncLoopHook.tap('2', (name, age) => {
console.log(2, name, age);
});
syncLoopHook.call('panda', 18);
// 执行结果:
// 1 panda 18
// 1 panda 18
// 1 panda 18
// 2 panda 18
同步循环钩子,和SyncHook
使用方式相同,当监听函数返回值为true时,会重复执行监听函数。
AsyncParallelHook
const {AsyncParallelHook} = require('tapable');
// 异步并行钩子,监听函数的执行是并行的,不会等待监听函数执行完毕
const asyncParallelHook = new AsyncParallelHook(['name', 'age']);
asyncParallelHook.tapAsync('1', (name, age, callback) => {
setTimeout(() => {
console.log(1, name, age);
callback();
}, 1000);
});
asyncParallelHook.tapAsync('2', (name, age, callback) => {
setTimeout(() => {
console.log(2, name, age);
callback();
}, 2000);
});
console.log('开始执行');
asyncParallelHook.callAsync('panda', 18, () => {
console.log('执行结束');
});
console.log('执行还没结束');
// 执行结果:
// 开始执行
// 执行还没结束
// 1 panda 18
// 2 panda 18
// 执行结束
上面的钩子都是Sync
开头的,表示同步钩子,现在是Async
开头的,表示异步钩子,异步钩子执行的回调函数中,最后一个参数是callback
,必须调用它,表示监听的函数执行完成。
同时异步的钩子还可以Promise
化,如下代码:
const {AsyncParallelHook} = require('tapable');
const asyncParallelHook = new AsyncParallelHook();
asyncParallelHook.tapPromise('1', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1);
resolve();
}, 100);
});
});
asyncParallelHook.tapPromise('2', () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(2);
resolve();
}, 200);
});
});
asyncParallelHook.callAsync(() => {
console.log('执行结束');
});
AsyncParallelBailHook
const {AsyncParallelBailHook} = require('tapable');
// 异步并行熔断钩子,当监听函数的返回值不为undefined时,后续的监听函数不再执行
const asyncParallelBailHook = new AsyncParallelBailHook(['name', 'age']);
asyncParallelBailHook.tapAsync('1', (name, age, callback) => {
setTimeout(() => {
console.log(1, name, age);
callback();
}, 1000);
});
asyncParallelBailHook.tapAsync('2', (name, age, callback) => {
setTimeout(() => {
console.log(2, name, age);
callback(2);
}, 2000);
});
asyncParallelBailHook.tapAsync('3', (name, age, callback) => {
setTimeout(() => {
console.log(3, name, age);
callback();
}, 3000);
});
asyncParallelBailHook.callAsync('panda', 18, () => {
console.log('执行结束');
});
// 执行结果:
// 1 panda 18
// 2 panda 18
// 执行结束
// 3 panda 18
这个和同步熔断钩子的解释相同,使用和异步并行钩子相同,但是熔断了,却没有完全熔断,应该算是bug吧。
AsyncSeriesHook
const {AsyncSeriesHook} = require('tapable');
// 异步串行钩子,监听函数的执行是串行的,会等待监听函数执行完毕
const asyncSeriesHook = new AsyncSeriesHook(['name', 'age']);
asyncSeriesHook.tapAsync('1', (name, age, callback) => {
setTimeout(() => {
console.log(1, name, age);
callback();
}, 1000);
});
asyncSeriesHook.tapAsync('2', (name, age, callback) => {
setTimeout(() => {
console.log(2, name, age);
callback();
}, 2000);
});
asyncSeriesHook.tapAsync('3', (name, age, callback) => {
setTimeout(() => {
console.log(3, name, age);
callback();
}, 3000);
});
asyncSeriesHook.callAsync('panda', 18, () => {
console.log('执行结束');
});
// 执行结果:
// 1 panda 18
// 2 panda 18
// 3 panda 18
// 执行结束
异步串行钩子,监听函数的执行是串行的,会等待监听函数执行完毕,也就是上面的代码整个执行时间需要6s。
剩下还有三个钩子,根据名称就可以推测出他们的作用了,也可以自己去写代码尝试一下,它的运行效果和执行逻辑。
实践
使用tapable
可以不一定要用在webpack
的插件开发上面,可以构建自己的应用系统上面,就比如我上面写钩子的示例,那个钩子没啥内容,现在就搞一个文件上传的tapable
版本:
const {SyncHook, SyncBailHook} = require('tapable');
class UploadFile {
hooks = {
// 可以在这个阶段控制临时修改选择文件的类型
openSelectDialog: new SyncHook(),
// 选择文件
selectFile: new SyncHook(),
// 上传文件之前
beforeUpload: new SyncBailHook(),
// 上传文件之后
afterUpload: new SyncHook(),
};
accept = '*';
maxFileSize = 1024 * 1024 * 10;
fileList = [];
constructor() {
}
// 选择文件
selectFile() {
this.hooks.openSelectDialog.call();
const input = document.createElement('input');
input.type = 'file';
input.accept = this.accept;
input.onchange = (e) => {
if (this.maxFileSize && e.target.files[0].size > this.maxFileSize) {
console.log('文件超过限制');
return;
}
this.fileList.push(e.target.files[0]);
this.hooks.selectFile.call(e.target.files[0], this.fileList);
};
input.click();
this.hooks.selectFile();
}
// 上传文件
uploadFile() {
// 通过钩子控制是否上传
this.hooks.afterUpload.tap('uploadFile', () => {
const formData = new FormData();
this.fileList.forEach((file) => {
formData.append('file', file);
});
// 上传文件
fetch('http://localhost:3000/upload', {
method: 'POST',
body: formData,
}).then((res) => {
console.log(res);
}).finally(() => {
this.hooks.afterUpload.call();
});
});
}
}
上面是没有经过测试的代码,很多东西都没有考虑,只是一个示例思路,肯定是一堆bug的。
总结
tapable
只是一个工具,具体怎么使用还是看开发者,不一定要使用在webpack
,但是对于学习webpack
,深入构建流程,了解插件开发是必不可少的一个环节,努力加油吧。
转载自:https://juejin.cn/post/7147601996319719454