如果这样理解, Tapable 真的很简单
在 Github 上 Tapable 的简介写的非常简单: “Just a little module for plugins.“ 但是很长时间以来,包括我自己在内,一直没有真正理解 Tapable 做的事请,其中的罪魁祸首就是 Webpack.当我们把 Webpack 和 Tapable 揉在一起时,本来很好理解的 Tapable 也变得复杂了,那我们就抛开 Webpack, 单独看一下 Tapable 吧!
任务目标
Tapable
是一个插件架构体系,同时也是一个实现任务管理的架构.它提供了若干类型的 hook
, 并可以在 hook 上注册插件.我们抛开 Webpack 来单独挖掘 tapable 的思想和价值,tapable 的核心是 hook, hook 代表了一个任务目标.我们可以创建一个任务目标,假设我们有一个任务目标:通过蓝牙获取目标设备的数据.我们可以创建这样一个 hook:
const fetchDataByBluetoothMission = new AsyncSeriesWaterfallHook(['missionId'])
此时fetchDataByBluetoothMission
就代表着一个任务目标,一个使命,“我要从一个设备中通过蓝牙获取一些数据”,然而要完成这个使命,是非常复杂的,需要非常非常多的步骤和处理逻辑,但是不管步骤和处理逻辑如何变化,这个 Hook 的最终任务目标是不会发生改变的!
执行细节
拥有了任务目标,接下来需要执行细节.在 tapable 中,任务目标的制定者,并非是任务的执行者,也就是说,任务的执行者要做什么事情,任务的制定者并不需要关心.最最重要的任务的执行者不要打扰任务的制定者!在传统的任务管理机制上我们常常采用的是中心化集成的方式,大致就是如下的这种方式:
function mission(initialParam) {
const result0 = execTask0(initialParam)
const result1 = execTask1(result0)
const result2 = execTask2(result1)
const result3 = execTask3(result2)
const result4 = execTask4(result3)
return result4
}
这种方式是最常见的方式,这里有一个显而易见的问题就是每当需要为任务目标的达成添砖加瓦时,除了实现我任务细节外,都需要去到任务调用的地方,把新的任务加进去...
这里我们借用 Webpack 来说一下,我们只是想针对我们的项目构建,实现一个具体的小功能(小插件),却要跑到 Webpack 项目包里改代码,这合理吗?
这 TM 当然不合理!而且很不合理!
所以 Tapable 来了,不合理的事情有了合理的解决方案!
构造一个 Tapable 的应用场景
我们仍然顺着上面我要从一个设备中通过蓝牙获取一些数据这个任务目标来说,假设为了完成这个任务目标,需要做以下的事情: 开启蓝牙 -> 搜寻附近可连接设备 -> 进行连接 -> 传输数据 -> 接受设备响应数据 -> 校验数据完整性(完成)
通过 Tapable 我们该怎么做呢?
首先创建一个任务目标:
const { AsyncSeriesWaterfallHook } = require('tapable')
// 创建一个任务目标,执行该任务时需要传入一个 missionId 作为参数
const fetchDataByBluetoothMission = new AsyncSeriesWaterfallHook(['missionId'])
// 记录当前最新的任务 ID
let freshMission
// 关于该任务目标下的任务有一些要求,当有新任务触发时,自动停止老任务的推进
fetchDataByBluetoothMission.intercept({
context: true,
// 触发事件时执行
call(context, ...args) {
context.mission = args[0]
freshMission = context.mission
},
// 事件回调调用前执行
tap(context, ...args) {
if (freshMission !== context.mission) {
context.failReason = `${context.mission} expired`
}
},
})
module.exports = fetchDataByBluetoothMission
接下来去实现任务细节吧!
// 钩入第一个任务-连接蓝牙
fetchDataByBluetoothMission.tapPromise(
{ name: 'turn on blue tooth', context: true },
async (context, ...params) => {
if (context.failReason) return Promise.reject(context.failReason)
return await asyncTask('turn on blue tooth')
},
)
// 钩入第二个任务-搜寻附近可连接设备
fetchDataByBluetoothMission.tapPromise(
{ name: 'search avaliable device', context: true },
async (context, ...params) => {
if (context.failReason) return Promise.reject(context.failReason)
return await asyncTask('search avaliable device')
},
)
// 钩入第三个任务-连接到设备
fetchDataByBluetoothMission.tapPromise(
{ name: 'connect device', context: true },
async (context, ...params) => {
if (context.failReason) return Promise.reject(context.failReason)
return await asyncTask('connect device')
},
)
// 钩入第四个任务-传输数据
fetchDataByBluetoothMission.tapPromise(
{ name: 'transfer data', context: true },
async (context, ...params) => {
if (context.failReason) return Promise.reject(context.failReason)
return await asyncTask('transfer data')
},
)
// 钩入第五个任务-接受设备响应数据
fetchDataByBluetoothMission.tapPromise(
{ name: 'receiveremote data', context: true },
async (context, ...params) => {
if (context.failReason) return Promise.reject(context.failReason)
return asyncTask('receive remote data')
.then(() => {
context.receivedData = 'AAAAAA_AAAAAA'
})
.catch(() => {
context.receivedData = 'BBBBBB_BBBBBB'
})
},
)
// 钩入地六个任务-校验数据完整性
fetchDataByBluetoothMission.tapPromise(
{ name: 'verify received data', context: true },
async (context, ...params) => {
if (context.failReason) return Promise.reject(context.failReason)
if (context.receivedData === 'AAAAAA_AAAAAA') {
return context.receivedData
} else {
return Promise.reject('verify failed')
}
},
)
所以 Tapable 做的就是这样一件简单的事情,把目标(hook)和任务(tap)极解耦了!
有了 Tapable,任务目标的制定者制定好任务目标后就可以做一个甩手掌柜,似乎再和任务执行者们说:你们爱怎么干就怎么干,别影响到我就可以!
那 Webpack 其实在一定程度上就是一个目标的制定者, Webpack 一点也不关心你的项目该怎么构建成想要的样子,但是 Webpack 通过 Tapable 雇用了一堆堆的 Plugin,是 Plugin 辛辛苦苦把项目构建好的.
这么看来 Tapable 所期望达成的效果也就比较清晰了,接下来我们来说一下它的一些细节概念.
Tapable 全面介绍
Hook 种类
在 Tapable 共有以下不同种类的 Hook
-
SyncHook 同步任务执行,没有任何特殊的地方,按照任务钩入的顺序和参数逐个执行
-
SyncBailHook 同步任务执行,与 SyncHook 类似,区别在于当任务中有任意一个任务出现返回值时,抛弃未执行的任务,提前结束 hook 的执行.
-
SyncWaterfallHook 同步任务执行,与 SyncHook 类似,区别在于每一个任务执行的返回值,会作为下一个任务执行的参数.
-
SyncLoopHook 同步任务执行,与 SyncHook 类似,区别在于当有任务出现返回值时,hook 会重新启动知道每一个任务都没有返回值.
-
AsyncParallelHook 异步任务并发执行
-
AsyncSeriesHook 异步任务串行执行
同时还有 AsyncParallelBailHook AsyncSeriesBailHook AsyncSeriesLoopHook AsyncSeriesWaterfallHook,通过名称应该已经能猜出每个 Hook 的功能了吧.
任务钩入方式
-
tap 钩入同步任务的方式, 与任务执行方式 call 对应
-
tapAsync 钩入异步任务的方式, 与任务执行方式 callAsync 对应,通过注册回调函数来获取任务执行结果
-
tapPromise 钩入异步任务的方式, 与任务执行方式 promise 对应,将会返回一个 promise
tap 插入任务,支持 stage 和 before 两个参数来控制任务执行顺序 before 若插入任务时存在 before 中的任务,将会插入在所有 before 任务之前 stage 在不考虑 before 的场景下,所有的任务插入顺序按照 stage 由小到大排序, stage 越小越先执行
同时支持 context 参数,当 context 参数传 true 时,每一个钩入的回调都可以拿到该 hook 的上下文.
Interceptor
Interceptor 让我们在 hook 执行的各个阶段能够定制一些功能
asyncSeriesLoopHook.intercept({
context: true,
// 在 hook 上注册任务时调用
register(...args) {},
// 触发hook执行时调用
call(...args) {},
// 在 call 拦截器之后执行,如果是 loop 任务,在每次 loop 开始前执行
loop(...args) {},
// hook上注册的任务执行前调用
tap(...args) {},
})
转载自:https://juejin.cn/post/7141187442391908360