webpack之初探Tapable

167 阅读3分钟

webpack 的强大,离不开 Tapable 库的功劳。在整个打包构建的流程中,会涉及很多个阶段,每个阶段都会调用相应的钩子。钩子里可以挂上插件,当触发钩子的 call 方法时,里面的插件就会一一被通知执行,而 Tapable 库就是提供这些钩子。

基本的流程如下:

  1. 注册:通过基类实例的 tap/ tapAsync/ tapPromise 方法注册对应的 hook 函数。

  2. 触发:通过 call/ callAsync/ promise 方法去触发钩子。

  3. 生成执行代码:不同触发方法传入 compile type 值不同。compile 再根据不同过的 type 生成可执行函数。

  4. 执行。

钩子的基类可以分为 Hook HookCodeFactory 。前者主要是收集并处理挂载在钩子上的 taps ,后者会根据前者返回的 options 生成执行钩子的代码。

以下篇幅主要是以 Hook 的基础种类的使用方法展开。

同步钩子

顾名思义就是按照时间顺序执行的钩子,代表有 SyncHookSyncBailHook、SyncWaterfallHook。

SyncHook

class People{
    constructor(){
        this.setInfo = SyncHook(["name","age"]);  // 表示这个hook需要多少个参数 
    }
    print(name, age){
        this.setInfo.call(name, age)
    }
}


let tomato = new People();
tomato.setInfo.tap('basicInfo', (...params)=>{
let [name, age] = params;
console.log(my name is <span class="hljs-subst">${name}</span>, I am <span class="hljs-subst">${age}</span> years old)
})
tomato.print('tomato', 12)




// output
// my name is tomato, I am 12 years old

// output // my name is tomato, I am 12 years old

SyncHook 的原理有点类似于 发布订阅 模式。tap 去注册消费者,call 去通知消费者执行

call 的实现方法有点绕,主要是通过拼接字符串的形式创建 new Function,再将编译好的 function 赋给 call,最后再按顺序依次执行插件。以下为简略版的发布订阅。

class SyncHook{
    constructor() {
        this.taps = [];
    }
    tap(options, fn){
        options = Object.assign({ type, fn }, options);
        let i = this.taps.length;
        while(i){
            //...
        }
        this.taps[i] = options;
    }
    call(...arg){
        let args = arg.join(", ");
        let fn = new Function(args, /** 拼接函数字符串 **/)
        for (let j = this.taps.length - 1; j >= 0; j--) {
            // ..按顺序遍历执行 fn
        }
    }
}

 

SyncBailHook

SyncBailHook 的特点在于如果遇到第一个返回为非 undefined 的事件则停止,不会继续走下去。

class People{
    constructor(){
        this.hook = {
            setName: SyncHook(["name"]),
            address: SyncBailHook(["name"])
        }
    }
    printAddress(name){
        this.hook.address.call(name);
    }
}


let tomato = new People();
tomato.hook.address.tap('province',(name) =>{
console.log("广东省")
})
tomato.hook.address.tap('city',(name) =>{
console.log("深圳市");
// 匹配到市就停止了
if(name.toString().indexOf("市")) return true;
})
tomato.hook.address.tap('area',(name) =>{
console.log("南山区");
})
tomato.printAddress("深圳市")




// output
// 广东省
// 深圳市

// output // 广东省 // 深圳市

 

SyncWaterfallHook

SyncWaterfallHook 有点类似于数组的 reduce 方法,上一个执行返回的结果作为第一个参数传给下一个插件。

class People{
    constructor(){
        this.hook = {
            income: SyncWaterfallHook(["num"])
        }
    }
    rest(number){
        this.hook.income.call(number);
    }
}


let tomato = new People();
tomato.hook.income.tap('Eat',(num) =>{
console.log(伙食支出:1000);
return num-1000
})
tomato.hook.income.tap('Clothes',(num) =>{
console.log(服饰支出:1000);
return num-1000
})
tomato.hook.income.tap('all',(num) =>{
console.log(剩余:<span class="hljs-subst">${num}</span>);
})
tomato.rest(5000)




// output
// 伙食支出:1000
// 服饰支出:1000
// 剩余:3000

// output // 伙食支出:1000 // 服饰支出:1000 // 剩余:3000

异步钩子

如果回调函数存在异步,则需要用到异步的钩子。代表有 AsyncSeriesHook、AsyncSeriesBailHook、AsyncSeriesWaterfallHook、AsyncParallelHook、AsyncParallelBailHook

 

AsyncSeriesHook

AsyncSeriesHook 是异步串行钩子,也就是异步函数会按顺序执行。

class People{
    constructor(){
        this.hook = {
            name: AsyncSeriesHook(["name"])
        }
    }
    sayName(name){
        return this.hook.name.promise(name);
    }
}


let tomato = new People();
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(my name is <span class="hljs-subst">${name}</span>)
resolve()
}, 2000)
})
})
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(my name2 is <span class="hljs-subst">${name}</span>)
resolve()
}, 1000)
})
})
tomato.sayName('tomato').then(() => { console.log("end") })




// output 总共花费3s
// my name is tomato
// my name2 is tomato
// end

// output 总共花费3s // my name is tomato // my name2 is tomato // end

 

AsyncSeriesBailHook

AsyncSeriesBailHook 也是异步串行钩子,如果遇到第一个返回值,则会立刻跳到最终的回调函数,不会继续执行接下来的插件。

class People{
    constructor(){
        this.hook = {
            name: AsyncSeriesBailHook(["name"])
        }
    }
    sayName(name){
        return this.hook.name.promise(name);
    }
}


let tomato = new People();
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(my name is <span class="hljs-subst">${name}</span>)
resolve(name) // !!重点
}, 2000)
})
})
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(my name2 is <span class="hljs-subst">${name}</span>)
resolve()
}, 1000)
})
})
tomato.sayName('tomato').then(() => { console.log("end") })




// output
// my name is tomato
// end

// output // my name is tomato // end

 

AsyncSeriesWaterfallHook

AsyncSeriesWaterfallHook 也是异步串行钩子,会将前一个的返回值传给下一个。

 

AsyncParallelHook

AsyncParallelHook 是异步并行钩子,等所有注册的异步任务都执行完,才会执行最后的回调,而所有的异步任务几乎是同时进行的。

class People{
    constructor(){
        this.hook = {
            name: AsyncParallelHook(["name"])
        }
    }
    sayName(name){
        return this.hook.name.promise(name);
    }
}


let tomato = new People();
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1 <span class="hljs-subst">${name}</span>,new Date())
resolve()
}, 2000)
})
})
tomato.hook.name.tapPromise('name',(name)=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(2 <span class="hljs-subst">${name}</span>, new Date())
resolve()
}, 1000)
})
})
tomato.sayName('tomato').then(() => { console.log("end",new Date()) })




// output
// 2 tomato Wed Oct 28 2020 16:55:52 GMT+0800 (中国标准时间)
// 1 tomato Wed Oct 28 2020 16:55:53 GMT+0800 (中国标准时间)
// end Wed Oct 28 2020 16:55:53 GMT+0800 (中国标准时间)

// output // 2 tomato Wed Oct 28 2020 16:55:52 GMT+0800 (中国标准时间) // 1 tomato Wed Oct 28 2020 16:55:53 GMT+0800 (中国标准时间) // end Wed Oct 28 2020 16:55:53 GMT+0800 (中国标准时间)

 

AsyncParallelBailHook

AsyncParallelBailHook 类似于 AsyncParallelHook,但是当第一个插件执行完resolve 不传 undefined 则立即熔断,直接调用最终的回调函数,其他异步的插件正常进行。

  • 如果所有插件的 resolve 不传参数,则效果和 AsyncParallelHook 一样
  • 如果第一个插件的 resolve undefined,则下一个 resolve 不会 undefined 的插件的值会传到最终的回调函数。
  • 如果 reject 不传参数,则不会执行到最终的 catch

 


tomato.hook.name.tapPromise('name',(name)=>{
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`1 ${name}`,new Date())
            resolve()
        }, 1000)
    })
})
tomato.hook.name.tapPromise('name',(name)=>{
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`2 ${name}`, new Date())
            resolve(name+'2')
        }, 2000)
    })
})
tomato.hook.name.tapPromise('name',(name)=>{
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`3 ${name}`, new Date())
        }, 2000)
    })
})
tomato.sayName('tomato').then((res) => { console.log("end", res) }).catch((err) => console.log("oops!",err))


// 1 tomato 2020-12-01T03:20:43.539Z
// 2 tomato 2020-12-01T03:20:44.545Z
// end tomato2
// 3 tomato 2020-12-01T03:20:44.545Z

// 1 tomato 2020-12-01T03:20:43.539Z // 2 tomato 2020-12-01T03:20:44.545Z // end tomato2 // 3 tomato 2020-12-01T03:20:44.545Z

 

webpack