天天看点

webpack核心模块tapable用法解析

前不久写了一篇webpack基本原理和AST用法的文章,本来想接着写<code>webpack plugin</code>的原理的,但是发现<code>webpack plugin</code>高度依赖tapable这个库,不清楚<code>tapable</code>而直接去看<code>webpack plugin</code>始终有点雾里看花的意思。所以就先去看了下<code>tapable</code>的文档和源码,发现这个库非常有意思,是增强版的<code>发布订阅模式</code>。<code>发布订阅模式</code>在源码世界实在是太常见了,我们已经在多个库源码里面见过了:

<code>redux</code>的<code>subscribe</code>和<code>dispatch</code>

<code>Node.js</code>的<code>EventEmitter</code>

<code>redux-saga</code>的<code>take</code>和<code>put</code>

这些库基本都自己实现了自己的<code>发布订阅模式</code>,实现方式主要是用来满足自己的业务需求,而<code>tapable</code>并没有具体的业务逻辑,是一个专门用来实现事件订阅或者他自己称为<code>hook</code>(钩子)的工具库,其根本原理还是<code>发布订阅模式</code>,但是他实现了多种形式的<code>发布订阅模式</code>,还包含了多种形式的流程控制。

<code>tapable</code>暴露多个API,提供了多种流程控制方式,连使用都是比较复杂的,所以我想分两篇文章来写他的原理:

先看看用法,体验下他的多种流程控制方式

通过用法去看看源码是怎么实现的

本文就是讲用法的文章,知道了他的用法,大家以后如果有自己实现<code>hook</code>或者事件监听的需求,可以直接拿过来用,非常强大!

本文例子已经全部上传到GitHub,大家可以拿下来做个参考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage

<code>tapable</code>是<code>webpack</code>的核心模块,也是<code>webpack</code>团队维护的,是<code>webpack plugin</code>的基本实现方式。他的主要功能是为使用者提供强大的<code>hook</code>机制,<code>webpack plugin</code>就是基于<code>hook</code>的。

下面是官方文档中列出来的主要API,所有API的名字都是以<code>Hook</code>结尾的:

这些API的名字其实就解释了他的作用,注意这些关键字:<code>Sync</code>, <code>Async</code>, <code>Bail</code>, <code>Waterfall</code>, <code>Loop</code>, <code>Parallel</code>, <code>Series</code>。下面分别来解释下这些关键字:

Sync:这是一个同步的<code>hook</code>

Async:这是一个异步的<code>hook</code>

Bail:<code>Bail</code>在英文中的意思是<code>保险,保障</code>的意思,实现的效果是,当一个<code>hook</code>注册了多个回调方法,任意一个回调方法返回了不为<code>undefined</code>的值,就不再执行后面的回调方法了,就起到了一个“保险丝”的作用。

Waterfall:<code>Waterfall</code>在英语中是<code>瀑布</code>的意思,在编程世界中表示顺序执行各种任务,在这里实现的效果是,当一个<code>hook</code>注册了多个回调方法,前一个回调执行完了才会执行下一个回调,而前一个回调的执行结果会作为参数传给下一个回调函数。

Loop:<code>Loop</code>就是循环的意思,实现的效果是,当一个<code>hook</code>注册了回调方法,如果这个回调方法返回了<code>true</code>就重复循环这个回调,只有当这个回调返回<code>undefined</code>才执行下一个回调。

Parallel:<code>Parallel</code>是并行的意思,有点类似于<code>Promise.all</code>,就是当一个<code>hook</code>注册了多个回调方法,这些回调同时开始并行执行。

Series:<code>Series</code>就是串行的意思,就是当一个<code>hook</code>注册了多个回调方法,前一个执行完了才会执行下一个。

<code>Parallel</code>和<code>Series</code>的概念只存在于异步的<code>hook</code>中,因为同步<code>hook</code>全部是串行的。

下面我们分别来介绍下每个API的用法和效果。

同步API就是这几个:

前面说了,同步API全部是串行的,所以这几个的区别就在流程控制上。

<code>SyncHook</code>是一个最基础的<code>hook</code>,其使用方法和效果接近我们经常使用的<code>发布订阅模式</code>,注意<code>tapable</code>导出的所有<code>hook</code>都是类,基本用法是这样的:

因为<code>SyncHook</code>是一个类,所以使用<code>new</code>来生成一个实例,构造函数接收的参数是一个数组<code>["arg1", "arg2", "arg3"]</code>,这个数组有三项,表示生成的这个实例注册回调的时候接收三个参数。实例<code>hook</code>主要有两个实例方法:

<code>tap</code>:就是注册事件回调的方法。

<code>call</code>:就是触发事件,执行回调的方法。

下面我们扩展下官方文档中小汽车加速的例子来说明下具体用法:

然后运行下看看吧,当加速事件出现的时候,会依次执行这三个回调:

webpack核心模块tapable用法解析

上面这个例子主要就是用了<code>tap</code>和<code>call</code>这两个实例方法,其中<code>tap</code>接收两个参数,第一个是个字符串,并没有实际用处,仅仅是一个注释的作用,第二个参数就是一个回调函数,用来执行事件触发时的具体逻辑。

上述这种写法其实与webpack官方文档中对于plugin的介绍非常像了,因为<code>webpack</code>的<code>plguin</code>就是用<code>tapable</code>实现的,第一个参数一般就是<code>plugin</code>的名字:

webpack核心模块tapable用法解析

而<code>call</code>就是简单的触发这个事件,在<code>webpack</code>的<code>plguin</code>中一般不需要开发者去触发事件,而是<code>webpack</code>自己在不同阶段会触发不同的事件,比如<code>beforeRun</code>, <code>run</code>等等,<code>plguin</code>开发者更多的会关注这些事件出现时应该进行什么操作,也就是在这些事件上注册自己的回调。

上面的<code>SyncHook</code>其实就是一个简单的<code>发布订阅模式</code>,<code>SyncBailHook</code>就是在这个基础上加了一点流程控制,前面我们说过了,<code>Bail</code>就是个保险,实现的效果是,前面一个回调返回一个不为<code>undefined</code>的值,就中断这个流程。比如我们现在将前面这个例子的<code>SyncHook</code>换成<code>SyncBailHook</code>,然后在检测超速的这个插件里面加点逻辑,当它超速了就返回错误,后面的<code>DamagePlugin</code>就不会执行了:

然后再运行下看看:

webpack核心模块tapable用法解析

可以看到由于<code>OverspeedPlugin</code>返回了一个不为<code>undefined</code>的值,<code>DamagePlugin</code>被阻断,没有运行了。

<code>SyncWaterfallHook</code>也是在<code>SyncHook</code>的基础上加了点流程控制,前面说了,<code>Waterfall</code>实现的效果是将上一个回调的返回值作为参数传给下一个回调。所以通过<code>call</code>传入的参数只会传递给第一个回调函数,后面的回调接受都是上一个回调的返回值,最后一个回调的返回值会作为<code>call</code>的返回值返回给最外层:

然后看下运行效果吧:

webpack核心模块tapable用法解析

<code>SyncLoopHook</code>是在<code>SyncHook</code>的基础上添加了循环的逻辑,也就是如果一个插件返回<code>true</code>就会一直执行这个插件,直到他返回<code>undefined</code>才会执行下一个插件:

执行效果如下:

webpack核心模块tapable用法解析

所谓异步API是相对前面的同步API来说的,前面的同步API的所有回调都是按照顺序同步执行的,每个回调内部也全部是同步代码。但是实际项目中,可能需要回调里面处理异步情况,也可能希望多个回调可以同时并行执行,也就是<code>Parallel</code>。这些需求就需要用到异步API了,主要的异步API就是这些:

既然涉及到了异步,那肯定还需要异步的处理方式,<code>tapable</code>支持回调函数和<code>Promise</code>两种异步的处理方式。所以这些异步API除了用前面的<code>tap</code>来注册回调外,还有两个注册回调的方法:<code>tapAsync</code>和<code>tapPromise</code>,对应的触发事件的方法为<code>callAsync</code>和<code>promise</code>。下面分别来看下每个API吧:

<code>AsyncParallelHook</code>从前面介绍的命名规则可以看出,他是一个异步并行执行的<code>Hook</code>,我们先用<code>tapAsync</code>的方式来看下怎么用吧。

还是那个小汽车加速的例子,只不过这个小汽车加速没那么快了,需要一秒才能加速完成,然后我们在2秒的时候分别检测是否超速和是否损坏,为了看出并行的效果,我们记录下整个过程从开始到结束的时间:

上面代码需要注意的是,注册回调要使用<code>tapAsync</code>,而且回调函数里面最后一个参数会自动传入<code>done</code>,你可以调用他来通知<code>tapable</code>当前任务已经完成。触发任务需要使用<code>callAsync</code>,他最后也接收一个函数,可以用来处理所有任务都完成后需要执行的操作。所以上面的运行结果就是:

webpack核心模块tapable用法解析

从这个结果可以看出,最终消耗的时间大概是2秒,也就是三个任务中最长的单个任务耗时,而不是三个任务耗时的总额,这就实现了<code>Parallel</code>并行的效果。

现在都流行<code>Promise</code>,所以<code>tapable</code>也是支持的,执行效果是一样的,只是写法不一样而已。要用<code>tapPromise</code>,需要注册的回调返回一个<code>promise</code>,同时触发事件也需要用<code>promise</code>,任务运行完执行的处理可以直接使用<code>then</code>,所以上述代码改为:

这段代码的逻辑和运行结果和上面那个是一样的,只是写法不一样:

webpack核心模块tapable用法解析

既然<code>tapable</code>支持这两种异步写法,那这两种写法可以混用吗?我们来试试吧:

这段代码无论我是使用<code>promise</code>触发事件还是<code>callAsync</code>触发运行的结果都是一样的,所以<code>tapable</code>内部应该是做了兼容转换的,两种写法可以混用:

webpack核心模块tapable用法解析

由于<code>tapAsync</code>和<code>tapPromise</code>只是写法上的不一样,我后面的例子就全部用<code>tapAsync</code>了。

前面已经看了<code>SyncBailHook</code>,知道带<code>Bail</code>的功能就是当一个任务返回不为<code>undefined</code>的时候,阻断后面任务的执行。但是由于<code>Parallel</code>任务都是同时开始的,阻断是阻断不了了,实际效果是如果有一个任务返回了不为<code>undefined</code>的值,最终的回调会立即执行,并且获取<code>Bail</code>任务的返回值。我们将上面三个任务执行时间错开,分别为1秒,2秒,3秒,然后在2秒的任务触发<code>Bail</code>就能看到效果了:

可以看到执行到任务2时,由于他返回了一个错误,所以最终的回调会立即执行,但是由于任务3之前已经同步开始了,所以他自己仍然会运行完,只是已经不影响最终结果了:

webpack核心模块tapable用法解析

<code>AsyncSeriesHook</code>是异步串行<code>hook</code>,如果有多个任务,这多个任务之间是串行的,但是任务本身却可能是异步的,下一个任务必须等上一个任务<code>done</code>了才能开始:

每个任务代码跟<code>AsyncParallelHook</code>是一样的,只是使用的<code>Hook</code>不一样,而最终效果的区别是:<code>AsyncParallelHook</code>所有任务同时开始,所以最终总耗时就是耗时最长的那个任务的耗时;<code>AsyncSeriesHook</code>的任务串行执行,下一个任务要等上一个任务完成了才能开始,所以最终总耗时是所有任务耗时的总和,上面这个例子就是<code>1 + 2 + 2</code>,也就是5秒:

webpack核心模块tapable用法解析

<code>AsyncSeriesBailHook</code>就是在<code>AsyncSeriesHook</code>的基础上加上了<code>Bail</code>的逻辑,也就是中间任何一个任务返回不为<code>undefined</code>的值,终止执行,直接执行最后的回调,并且将这个返回值传给最终的回调:

这个执行结果跟<code>AsyncParallelBailHook</code>的区别就是<code>AsyncSeriesBailHook</code>被阻断后,后面的任务由于还没开始,所以可以被完全阻断,而<code>AsyncParallelBailHook</code>后面的任务由于已经开始了,所以还会继续执行,只是结果已经不关心了。

webpack核心模块tapable用法解析

<code>Waterfall</code>的作用是将前一个任务的结果传给下一个任务,其他的跟<code>AsyncSeriesHook</code>一样的,直接来看代码吧:

运行效果如下:

webpack核心模块tapable用法解析

<code>tapable</code>是<code>webpack</code>实现<code>plugin</code>的核心库,他为<code>webpack</code>提供了多种事件处理和流程控制的<code>Hook</code>。

这些<code>Hook</code>主要有同步(<code>Sync</code>)和异步(<code>Async</code>)两种,同时还提供了阻断(<code>Bail</code>),瀑布(<code>Waterfall</code>),循环(<code>Loop</code>)等流程控制,对于异步流程还提供了并行(<code>Paralle</code>)和串行(<code>Series</code>)两种控制方式。

<code>tapable</code>其核心原理还是事件的<code>发布订阅模式</code>,他使用<code>tap</code>来注册事件,使用<code>call</code>来触发事件。

异步<code>hook</code>支持两种写法:回调和<code>Promise</code>,注册和触发事件分别使用<code>tapAsync/callAsync</code>和<code>tapPromise/promise</code>。

异步<code>hook</code>使用回调写法的时候要注意,回调函数的第一个参数默认是错误,第二个参数才是向外传递的数据,这也符合<code>node</code>回调的风格。

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

欢迎关注我的公众号进击的大前端第一时间获取高质量原创~

“前端进阶知识”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd

“前端进阶知识”系列文章源码GitHub地址: https://github.com/dennis-jiang/Front-End-Knowledges

webpack核心模块tapable用法解析

继续阅读