显微镜下的webpack4:灵魂tapable,终于搞懂钩子系列!

时间:2021-10-13 19:59:39

简介

大家在看webpack源码的时候,有没有感觉像再看天书,似乎没有办法一个文件比如webpack.js从头看到尾。感觉webpack的跳跃性很强,完全不知道程序在运行的时候,发生了什么。完全不清楚这个事件是什么时候发生的,比如loader是什么时候执行的,plugin又是什么时候出现的。webpack的程序错综复杂,完全迷失在程序之中。这究竟是为什么呢?其实很简单!因为webpack的灵魂Tapable!这个机制使得webpack异常的灵活,它有一句经典的话——Everything is a plugin!。由此可见webpack是靠插件堆积起来的。而实现这个插件机制的就是Tabable!

Events

webpack的灵魂Tapable,有点类似于nodejs的Events,都是注册一个事件,然后到了适当的时候触发。这里的事件触发是这样绑定触发的,通过on方法,绑定一个事件,emit方法出发一个事件。Tapable的机制和这类似,也是tap注册一个事件,然后call执行这个事件。

const EventEmitter = require('events');
const myEmitter = new EventEmitter();
//on的第一个参数是事件名,之后emit可以通过这个事件名,从而触发这个方法。
//on的第二个参数是回掉函数,也就是此事件的执行方法
myEmitter.on('newListener', (param1,param2) => {
    console.log("newListener",param1,param2)
});
//emit的第一个参数是触发的事件名
//emit的第二个以后的参数是回调函数的参数。
myEmitter.emit('newListener',111,222);

Tapable究竟为何物

如果我们把Tapable的实例对象比作一颗参天大树,那么的每一根树枝就是一个挂载的hook(钩子),也就是Tapable之中给每一个事件分门别类的机制,比如编译(compile.js)这个对象中,又运行(run)的钩子,有构建(make)的钩子,这些钩子就像树枝一样,组成了一棵树的骨干,然后每个树枝上的树叶就是每个钩子上面挂载的函数方法。树枝(钩子)越多,树叶(函数)越多,此树越茂密(程序越复杂)。

当然这只是一个简易的理解。实际上,webpack中不止有一棵树,每棵树之间还有错综复杂的关系。比如有些方法如compilation.js中的一些方法,就要等compile.js中的make这个钩子执行之后才会执行。那么我们就从了解Tapable中钩子的用法,来理解webpack中tapable。

以工作日为例,了解Tapable的用法

即使webpack中的每颗tapable的树之间有错综复杂的关系,整个程序都有一个逻辑线,也就是游戏中的主线剧情,我们先构建我们工作日的主线剧情。

主线剧情

让我们来回一下,我们的日常工作日,应该大多数分成3个阶段,上班前,上班中和下班后,这3个时间段。这三个时间段,我用了3中钩子类型,普通型,流水型和熔断型。
按照文档他们的解释是这样的:

  • 普通型basic:这个比较好理解就是按照tap的注册顺序一个个向下执行。
  • 流水型water:这个相对于basic的区别就是,虽然也是按照tap的顺序一个个向下执行,但是如果上一个tap有返回值,那么下一个tap的传入参数就是上一个tap的返回值。
  • 熔断型bail:这个相对于water的区别就是,如果返回了null以外的值,就不继续执行了。

是不是感觉一个事件的订阅发布怎么可以分出这么多类型?不要急,每个类型都有他的作用!

钩子的语法一般都是new 钩子类型Hook([参数名1,参数名2,参数名3]),这里的数组是只是提示你传入参数有几个,给了名字只是为了可读性,如果你想写一个别人看不懂的可以这样new SyncHook(["a","b","c"]),这里要注意这个参数名的类型是字符串。如果没有提前准备号需要传入的参数,后续挂函数的时候,就无法传入参数了。这个设计应该是为了日后好打理,告诉其他开发者,我传入的参数类型。

class MyDaily {
    constructor() {
        this.hooks = {
            beforeWork: new SyncHook(["getUp"]),
            atWork: new SyncWaterfallHook(["workTask"]),
            afterWork: new SyncBailHook(["activity"])
        };
    }
    tapTap(){
        //此处是行为
    }
    run(){
        this.hooks.beforeWork.call()
        this.hooks.atWork.call()
        this.hooks.afterWork.call()
    }
}

一天我们不可能什么事都不做,所以给钩子上加点事,tap事情。先来点必然发生的,正常的上班族,*职业不在考虑范围内。早上我们会做什么呢?穿衣服出门是必备的,不穿衣服没法出门,不出门没法上班。到了工作岗位,来点工作任务吧,比如我们需要做个ppt,然后用这个ppt去开会。下班后,本来想回家的,结果佳人有约,果然不回家。

tapTap(){
    this.hooks.beforeWork.tap("putOnCloth",()=>{
        console.log("穿衣服!")
    })
    this.hooks.beforeWork.tap("getOut",()=>{
        console.log("出门!")
    })
    this.hooks.atWork.tap("makePPT",()=>{
        console.log("做PPT!")
        return "你的ppt"
    })
    this.hooks.atWork.tap("meeting",(work)=>{
        console.log("带着你的"+work+"开会!")
    })
    this.hooks.afterWork.tap("haveADate",()=>{
        console.log("约会咯!")
        return "约会真开心~"
    })
    this.hooks.afterWork.tap("goHome",()=>{
        console.log("溜了溜了!")
    })
}

从上述我们可以看到通过主演剧情了解到各种同步钩子的用法,可能难以理解就是熔断型的钩子,这个钩子的存在意义就是,可以中断一系列的事情,比如有地方出错了,或者不需要进行下一步的操作我们就可以及时结束。

那么如果我们做的事情都是异步的,每一个事件之间都有联系,那么我们就不能用同步的方法了。这个时候我们可以将sync钩子替换成async的钩子。

async相对于sync多了一个callback的机制,就是这样的:

this.hooks.beforeWork.tapAsync("putOnCloth",(params,callback)=>{
    console.log("穿衣服!")
    callback();//此处无callback,则getOut这个挂载的函数便不会运行
})
this.hooks.beforeWork.tapAsync("getOut",(params,callback)=>{
    console.log("出门!")
    callback()//此处无callback,则beforeWork这个钩子的回调函数不会执行
})
this.hooks.beforeWork.callAsync("working",err=>{
    console.log(err+" end!")//如果最后一个tap的函数没有callback则不会执行
})

这里我们可以将callback当作next函数,也就是下一个tap的函数的意思。以及如果当前tap的函数报错,则可以在callback中加入错误的原因,那么接下来的函数便不会运行,也就是这样callback("errorReason"),那么就直接回调用当前钩子的callAsync绑定的函数。

this.hooks.beforeWork.tapAsync("putOnCloth",(params,callback)=>{
    console.log("穿衣服!")
    callback("error");此处加入了错误原因,那么直接callAsync,抛弃了getOut
})
this.hooks.beforeWork.tapAsync("getOut",(params,callback)=>{//直接skip了
    console.log("出门!")
})
this.hooks.beforeWork.callAsync("working",err=>{
    console.log(err+" end!")//error end!直接打出错误原因。
})

小tips

大家发现没有,Async和sync的区别在于Async通过callback来和后续的函数沟通,sync则是通过return一个值来做交流。所以,Async自带sync中bail类型的钩子。我曾经做了一个无聊的统计,因为钩子太多了,我写了一个代码遍历了webpack这个项目,得出了所有钩子的使用情况,结果如下所示:

SyncHook 69
SyncBailHook 63
SyncWaterfallHook 36
SyncLoopHook 0
AsyncParallelHook 1
AsyncParallelBailHook 0
AsyncSeriesHook 14
AsyncSeriesBailHook 0
AsyncSeriesWaterfallHook 5

但是我发现AsyncSeriesBailHook竟然是0的时候,我很震惊,现在知道原因了,因为从作用上来说他和异步钩子的共能本身就重叠了,所以同理AsyncParallelBailHook这个平行执行的bail类型的钩子也是0 。bail在Async中功能重复,因次用的很少。

言归正传,既然AsyncSeriesHook的callback通过第一个err参数来判断是否异步成功,不成功则直接callAsync回调。那么water类型的该如何传递参数?我们都知道water和basic的区别就在于basic每个异步tap之间并无参数传递,而water则是参数传递。很简单,在err后面再加一个参数,作为下一个tap的传入值。

this.hooks.atWork.tapAsync("makePPT",(work,callback)=>{
    console.log("做PPT!")
    callback("没做完 ","你的ppt")//第一个参数是err,上交你的报错,第二个参数是你自定义要下一个tap处理的参数。如果有err,则忽略此参数。
})
this.hooks.atWork.tapAsync("meeting",(work,callback)=>{//因为ppt没做完,所以开不了会
    console.log("带着"+work+"开会!")
    callback()
})
this.hooks.atWork.callAsync("working",err=>{//没做完来这里领罚了。
    console.log(err+" end!")
})

支线剧情

我们的日常生活!才不会这么单调,怎么会一路顺顺利利地走下来呢?每天都有不同的精彩啊!小插曲肯定少不了。

那么我们就要相办法将支线剧情插入主线剧情之中了。这个时候一个MyDaily的类已经放不下我们的精彩生活了。

以下班之后的精彩为例,我们不一定会直接回家也有可能约会蹦迪什么的。所以这里我们new一个名为Activity的类。假设我们的夜生活有两个活动,一个派对活动,一个回家。那么派对活动肯定有个流程,我们就用熔断型的,为什么呢!不开心了接下来就都别执行了!回家吧!回家的这个活动就是简单的钩子。

class Activity {
    constructor() {
        this.hooks = {
            goParty:new SyncBailHook(),
            goHome:new SyncHook()
        };
    }
    prepare(){
        this.hooks.goParty.tap("happy",()=>{
            console.log("happy party")
        })
        this.hooks.goParty.tap("unhappy",()=>{
            console.log("unhappy")
            return "go home"
        })
        this.hooks.goParty.tap("play",()=>{
            console.log("continue playing")
        })
        this.hooks.goHome.tap("goHome",()=>{
            console.log("I'm going to sleep")
        })
    }
    start(){
        this.hooks.goParty.call()
        this.hooks.goHome.call()
    }
}

然后我们要将这个单独的类挂到MyDaily的下面,毕竟这也是日常的一部分虽然非正式关卡。我们可以在工作结束自开始准备晚上的活动,等到一下班就开始我们丰富的夜生活。这个时候我们可以在钩子的回调函数中触发另一个类中的钩子状态,激活或着运行。

class MyDaily {
    constructor() {
        this.hooks = {
            ....
        };
        this.activity=new Activity()//实例化Activity
    }
    run(){
        ...
        this.hooks.atWork.callAsync("working",res=>{
            this.activity.prepare()//下班了大家可以躁动起来了
        })
        this.hooks.afterWork.callAsync("activity",err=>{
            this.activity.start()//去浪咯!
        })
    }
}

总结

我在这里只是举了一个小例子,带大家理解tapable是什么。因为理解了tapable的特性,我们才能在之后有办法理解webpack的机制,因为这种钩子套钩子的原因,我们很难看懂webpack的源代码。下一篇文章我会带大家看懂webpack的主线剧情和主要支线剧情(loader&plugin)的流程!