tapable
是官方提供的一个核心工具,不仅可以用于wepback
当中,还可以使用在任何你需要的地方,webpack
很多类都扩展自tapable
,所以学习它对于学习webpack
有很大的能帮助,文档在这=>Tapable。
认识Tapable
官方对tapable
的定义是钩子,业界很多写tapable
的觉得它是发布订阅模式,它确实很像发布订阅模式,但是不完全是,至于官方说的钩子,个人感觉在webpack
内部可以称作钩子,因为是官方定义的,所以肯定是按照设计的想法来使用的,如果是你自己使用,操作不当可能就不是钩子了,毕竟只是一个工具,你拿扳手当锤子也没人说你什么,以上是个人见解,下面附上一些验证的思路。
不喜欢论证,喜欢看使用的可以跳过这个小标题,到第二个标题
tapable使用详解
。
1. 发布订阅模式
先来看看什么是发布订阅,发布订阅是需要分为两个部分,一个是发布,一个是订阅。
- 发布
发布者就是我,我写下这篇文章,点击发布就是发布了。
- 订阅
订阅者是你,但是你要先关注我,关注我之后我发布文章就会通知你。
这个中间还需要有一个平台来维护我们之间的关系,这个就是掘金,一顿分析下来,模型就长下面这样,图画的有点丑,应该不影响观看
对应转化成代码应该是下面这样的:
/** * 发布订阅模式 */ // 掘金是调度中心 class Jujin { // 掘金的所有用户 users = {}; // 注册用户 registerUser(userInfo) { const user = new User(userInfo); // 内部注册的用户才有权限订阅 user.subscribe = (uuid, callback) => { user.subscribes[uuid] = callback; } // 内部注册的用户才有权限发布消息 user.publish = (message) => { this.#publish(user.uuid, message); } this.users[user.uuid] = user; return user; } #publish(uuid, message) { const users = Object.values(this.users); for (let user of users) { const callback = user.subscribes[uuid]; if (callback) { callback(message); } } } } class User { constructor(userInfo) { this.name = userInfo.name; this.age = userInfo.age; this.uuid = Math.random().toString(36).substr(2); // 订阅的用户 this.subscribes = {}; } } // 掘金上线啦 const jujin = new Jujin(); // 我在掘金上注册了一个账号 const 田八 = jujin.registerUser({ name: '田八', age: 18 }); // 你在掘金上注册了一个账号 const 你 = jujin.registerUser({ name: '你', age: 18 }); // 你关注了我 你.subscribe(田八.uuid, (message) => { console.log(message); }); // 我发布了一篇文章 田八.publish('你好,我是田八!'); // 路人甲在掘金上注册了一个账号 const 路人甲 = jujin.registerUser({ name: '路人甲', age: 18 }); // 路人甲发布了一篇文章 路人甲.publish('你好,我是路人甲!'); // 可惜路人甲没有人关注,所以没有人看到他的文章
执行结果
代码不是正儿八经的发布订阅模式写的,但是思想还是差不多的,主要是为了还原我自己画的图,正儿八经的发布订阅模式有三个模块,一个是发布者,一个是订阅者,一个调度中心,我这里发布者和订阅者合并到一起了,调度中心还分管着权限。
这里主要看使用方式,最典型的就是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
,深入构建流程,了解插件开发是必不可少的一个环节,努力加油吧。