Tapable

216 阅读5分钟

想了解webpack的插件机制,了解Tapable 就很有必要。Tapable 的官方解释就是:一个暴露了很多Hooks,用来插件调用的包。

Tapable

const {
    SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook,
    AsyncParallelHook, AsyncParallelBailHook, 
    AsyncSeriesHoos, AsyncSeriesBailHook, AsyncSeriesWaterfallHook,
    HookMap, MultiHook
} = require('tapable')
  • 分类

    • Hook分为4个同步钩子和5个异步钩子
    • HookMapMultiHook都是Tapable 导出的辅助类
  • Hook 生成方式

    Hook接收一个可选的Array<string> 作为参数来构造

  • 示例

    const synchook = new SyncHook()
    const synchook1 = new SyncHook(['arrg1', 'arg2])
    ...
    
  • 最佳实践

    最好在一个类中用hooks 属性去初始化所有的钩子:

    class Car {
        constructor(args) {
            this.hooks = {
              startHook: new SyncHook(),
              syncBailHook: new SyncBailHook(),
              syncWaterFallHook: new SyncWaterfallHook(['arg1', 'arg2']),
              syncLoopHook: new SyncLoopHook(),
              asyncParallelHook: new AsyncParallelHook(['arg1', 'arg2']),
              asyncParallelBailHook: new AsyncParallelBailHook(['arg1', 'arg2']),
              asyncSeriesHook: new AsyncSeriesHook(['arg1', 'arg2']),
              asyncSeriesBailHook: new AsyncSeriesBailHook(['arg1', 'arg2']),
              asyncSeriesWaterfallHook: new AsyncSeriesWaterfallHook(['arg1', 'arg2'])
            }
        }
        ...
    }
    

    初始化定义好了,那怎么去使用一个钩子呢,既然是钩子的概念,就逃不过注册、触发这俩关键词,接下来我们看下Hook的使用方式

Hook 使用

  • 同步钩子: 注册: tap, 调用: call
  • 异步钩子: 注册: tap, tapPromise, tapAsync, 调用方式: call, promise, callAsync
  • 示例:

1. SyncHook

 class Car {
    constructor(args) {
       this.hooks = {
          startHook: new SyncHook(),
          syncBailHook: new SyncBailHook(),
          syncWaterfallHook: new SyncWaterfallHook(['arg1', 'arg2']),
          syncLoopHook: new SyncLoopHook(),
          asyncParallelHook: new AsyncParallelHook(['arg1', 'arg2']),
          asyncParallelBailHook: new AsyncParallelBailHook(['arg1', 'arg2']),
          asyncSeriesHook: new AsyncSeriesHook(['arg1', 'arg2']),
          asyncSeriesBailHook: new AsyncSeriesBailHook(['arg1', 'arg2']),
          asyncSeriesWaterfallHook: new AsyncSeriesWaterfallHook(['arg1', 'arg2'])
       }
    }
    start() {
        this.hooks.startHook.call()
    }
    
}
const carInstance = new Car()
// 注册
carInstance.hooks.startHook.tap('startPlugin1', () => {
  console.log('trigger startPlugin1 callback')
})
carInstance.hooks.startHook.tap('startPlugin2', () => {
  console.log('trigger startPlugin2 callback')
})
// 调用
carInstance.start()

输出结果如下:

image.png

可以看到同步钩子SyncHook会按照注册顺序按序执行

2. SyncBailHook

class Car {
    ,,,
    bail() {
        this.hooks.syncBailHook.tap()
    }
}
,,,
// 注册
carInstance.hooks.syncBailHook.tap('syncBailPlugin1', () => {
  console.log('trigger bailPlugin1 callback')
})
carInstance.hooks.syncBailHook.tap('syncBailPlugin2', () => {
  console.log('trigger bailPlugin2 callback')
  return 1
})
carInstance.hooks.syncBailHook.tap('syncBailPlugin3', () => {
  console.log('trigger bailPlugin3 callback')
})
// 调用
carInstance.bail()

输出结果:

image.png

  • 小结 SyncBailHook 的特点是:回调函数中返回非undefined值时,钩子会停止执行。

如果将上述例子中syncBailPlugin2回调函数修改为return undefined, 则输出结果如下:

image.png

3. SyncWaterfallHook

class Car {
    ,,,
    waterfall(arg1, arg2) {
        this.hooks.syncWaterfallHook.call(arg1, arg2)
    }
}
,,,
// 注册
carInstance.hooks.syncWaterfallHook.tap('syncWaterfallPlugin1', () => {
    console.log('triggered syncWaterfallPlugin1 and return value 1')
    return 1
})
carInstance.hooks.stncWaterfallHook.tap('syncWaterfallPlugin2', (prev) => {
    console.log(`triggered syncWaterfallPlugin2, receive prev result: ${prev} and return ++prev`)
    return ++prev
})
carInstance.hooks.stncWaterfallHook.tap('syncWaterfallPlugin3', (prev) => {
    console.log(`triggered syncWaterfallPlugin3, receive prev result: ${prev} and return ${++prev}`)
    return ++prev
})
// 调用
carInstance.hooks.waterfall()

输出结果:

image.png

再看一个有意思的情况,改动syncWaterfallPlugin2 的回调函数为:

carInstance.hooks.stncWaterfallHook.tap('syncWaterfallPlugin2', (prev) => {
    console.log(`triggered syncWaterfallPlugin2, receive prev result: ${prev} and return undefined`)
    return undefined
})

得到的结果是:

image.png 上述例子中,我们调用waterfall的时候并没有初始化arg1arg2,我们换一种调用方式看看:

carInstance.hooks.syncWaterFallHook.tap('syncWaterfallPlugin1', (arg1, arg2) => {
  console.log(`triggered syncWaterfallPlugin1, arg1: ${arg1}, arg2: ${arg2}`)
  arg2 = 3
  return 2
})
carInstance.hooks.syncWaterFallHook.tap('syncWaterfallPlugin2', (arg1, arg2) => {
  console.log(`triggered syncWaterfallPlugin2, arg1: ${arg1}, arg2: ${arg2}`)
  return arg2
  // console.log(`triggered syncWaterfallPlugin2, receive prev result: ${prev} and return undefined`)
  // return undefined
})
carInstance.hooks.syncWaterFallHook.tap('syncWaterfallPlugin3', (arg1, arg2) => {
  console.log(`triggered syncWaterfallPlugin3, arg1: ${arg1}, arg2: ${arg2}`)
  return ++arg1
})
carInstance.waterfall(1, 'second')

输出结果:

image.png

  • 小结

    1. 可以看到 SyncWaterfallHook 瀑布钩子会传递最近的上一个返回值为非undefined钩子的返回值作为下一个钩子的参数
    2. SyncWaterfallHook 钩子多个参数时,只能修改第一个参数,修改方式不是赋值,而是依赖每个钩子的返回值,每个钩子的非undefined返回值会作为第一个参数的最新值

4. SyncLoopHook

class Car {
    ,,,
    syncLoop() {
        this.hooks.syncLoopHook.call()
    }
}
// 注册
let index = 1
carInstance.hooks.syncLoopHook.tap(`syncLoopHookPlugin1`, () => {
  console.log(`trggered syncLoopPlugin1 启动: ${index}次`)
  if (index < 4) {
    index++
    return index
  }
})
// 调用
carInstance.syncLoop()

输出的结果是:

image.png

再看一个🌰:

,,,
// 注册
let index = 1
carInstance.hooks.syncLoopHook.tap(`syncLoopHookPlugin1`, () => {
  console.log(`trggered syncLoopPlugin1 启动: ${index}次`)
  if (index < 4) {
    index++
    return index
  }
})
carInstance.hooks.syncLoopHook.tap('syncLoopHookPlugin2', () => {
  console.log(`triggered syncLoopHookPlugin2 ${index} 次`)
  index = 2
  if (index < 4) {
    index++
    return index
  }
})
carInstance.hooks.syncLoopHook.tap('syncLoopHookPlugin3', () => {
  console.log('triggered syncLoopHookPlugin3')
})
// 调用
carInstance.syncLoop()

上个例子中我们注册了多个syncLoopHook,且动态去修改了index 的值,那我们看下输出结果是:

image.png

可以看到由于我们在syncLoopHookPlugin2的回调函数中修改了index的值,得到的结果就是 syncLoopPlugin1syncLoopHookPlugin2 循环调用

  • 小结 SyncLoopHook 同步循环钩子会在任何一个被监听的函数中返回非 undefined值时,返回重头开始执行。 (这也是我们看到上述例子中并没有触发执行syncLoopHookPlugin3

5. AsyncParallelHook

class Car {
    ,,,
    asyncParallelHookCallFun(arg1, arg2, cb) {
      this.hooks.asyncParallelHook.callAsync(arg1, arg2, cb)
      // return this.hooks.asyncParallelHook.promise()
    }
}
// 注册
console.time('timer')
carInstance.hooks.asyncParallelHook.tapAsync('asyncParallelPlugin1', (arg1, arg2, callback) => {
  setTimeout(_ => {
    console.log('triggered AsyncParallelPlugin1', arg1, arg2)
    callback()
  }, 1000)
})
carInstance.hooks.asyncParallelHook.tapPromise('asyncParallelPlugin2', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('triggered AsyncParallelPlugin2', arg1, arg2)
      resolve(true)
    }, 1000)
  })
})

carInstance.hooks.asyncParallelHook.tapPromise('asyncParallelPlugin3', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('triggered AsyncParallelPlugin3', arg1, arg2)
      resolve(false)
    }, 1000)
  })
})
carInstance.asyncParallelHookCallFun(1,2, () => {
  console.log('AsyncParallelHooks 全部执行完毕')
  console.timeEnd('timer')
})

输出结果:

image.png

修改下上述asyncParallelPlugin2的回调函数:

,,,
carInstance.hooks.asyncParallelHook.tapPromise('asyncParallelPlugin2', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('triggered AsyncParallelPlugin2', arg1, arg2)
      reject(true)
    }, 1000)
  })
})

则输出结果变为:

image.png

  • 小结 AsyncParallelHook异步并行钩子,会在被监听函数中,截止到最先满足条件的钩子执行完后触发执行(并不一定是所有钩子都执行完了),但即使回调函数执行完,后续未执行的钩子也会继续执行

6. AsyncParallelBailHook

class Car {
     asyncParallelBailHookCallFunc(arg1, arg2, cb) {
        this.hooks.asyncParallelBailHook.callAsync(arg1, arg2, cb)
        // return this.hooks.asyncParallelBailHook.promise()
      }
}
carInstance.hooks.asyncParallelBailHook.tapPromise('asyncParallelBailPlugin1', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('triggered asyncParallelBailPlugin1', arg1, arg2)
      resolve(undefined)
    }, 1000)
  })
})
// 注册
carInstance.hooks.asyncParallelBailHook.tapAsync('asyncParallelBailPlugin2', (arg1, arg2) => {
  setTimeout(_ => {
    console.log('triggered asyncParallelBailPlugin2', arg1, arg2)
  }, 1000)
})
// 调用
carInstance.asyncParallelBailHookCallFunc(1, 2, () => {
  console.log('AsyncParallelHooks 全部执行完毕')
  console.timeEnd('timer')
})

输出结果:

image.png 由于我们第一个 asyncParallelBailPlugin1 返回的是 resolve(undefind),所有会继续执行,如果修改为resolve(true), 那么输出结果为:

image.png 可以看到回调函数会在第一个钩子执行完后触发,但是第二个钩子也会继续执行

  • 小结 AsyncParallelBailHook 异步并行保释钩子,可以理解为保险丝,当返回undefined值时,会继续执行,返回非undefined值时,会触发最终回调函数的执行

7. AsyncSeriesHook

class Car {
    asyncSeriesHookCallFunc(arg1, arg2) {
        this.hooks.asyncSeriesHook.tapPromise(arg1, arg2)
    }
}
// 注册
carInstance.hooks.asyncSeriesHook.tapPromise('asyncSeriesHookPlugin1', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('triggered asyncSeriesHookPlugin1', arg1, arg2)
      resolve(6)
    }, 1000);
  })
})
carInstance.hooks.asyncSeriesHook.tapPromise('asyncSeriesHookPlugin1', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('triggered asyncSeriesHookPlugin1', arg1, arg2)
      resolve(5)
    }, 1000)
  })
})
// 调用
carInstance.asyncSeriesHookCallFunc(1, 2).then(res => {
  console.log('全部执行完毕, ', res)
  console.timeEnd('timer')
})

输出结果

image.png 如果将上述例子修改为

,,,
carInstance.hooks.asyncSeriesHook.tapPromise('asyncSeriesHookPlugin1', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('triggered asyncSeriesHookPlugin1', arg1, arg2)
      reject(false)
    }, 2000);
  })
})
carInstance.hooks.asyncSeriesHook.tapPromise('asyncSeriesHookPlugin1', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('triggered asyncSeriesHookPlugin1', arg1, arg2)
      resolve(5)
    }, 1000)
  })
})
carInstance.asyncSeriesHookCallFunc(1, 2).then(res => {
  console.log('全部执行完毕, ', res)
  console.timeEnd('timer')
}).catch(err => {
  console.log('catch series hooks error', err)
})

则输出结果为

image.png

  • 小结 AsyncSeriesHook 异步串行钩子会根据声明顺序串行执行,截止到reject状态或者tapAsync方式时的callback具备第一个参数表示错误,则终止执行(后钩子不会再执行)到最终回调函数的catch中。

8. AsyncSeriesBailHook

class Car {
 ,,,
 asyncSeriesBailHookCallFunc(arg1, arg2, cb) {
    this.hooks.asyncSeriesBailHook.callAsync(arg1, arg2, cb)
    // return this.hooks.asyncSeriesBailHook.promise()
  }
}
// 注册
carInstance.hooks.asyncSeriesBailHook.tapAsync('AsyncSeriesBailPlugin1', (arg1, arg2, callback) => {
  setTimeout(() => {
    console.log('triggered AsynsSeriesBailPlugin1 , args is: -- ', arg1, arg2)
    callback()
  }, 2000);
})
carInstance.hooks.asyncSeriesBailHook.tapPromise('AsyncSeriesBailPlugin2', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('triggered AsynsSeriesBailPlugin2 , args is: -- ', arg1, arg2)
      resolve()
    }, 1000)
  })
})
carInstance.hooks.asyncSeriesBailHook.tapPromise('AsyncSeriesBailPlugin3', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('triggered AsynsSeriesBailPlugin3 , args is: -- ', arg1, arg2)
      resolve(arg1)
    }, 1000)
  })
})
// 调用
carInstance.asyncSeriesBailHookCallFunc(1, 2, (result) => {
  console.log('全部执行完毕', result)
  console.timeEnd('timer')
})

输出结果是

image.png

如果将上述例子修改为:

carInstance.hooks.asyncSeriesBailHook.tapAsync('AsyncSeriesBailPlugin1', (arg1, arg2, callback) => {
  setTimeout(() => {
    console.log('triggered AsynsSeriesBailPlugin1 , args is: -- ', arg1, arg2)
    callback(arg1)
  }, 2000);
})
carInstance.hooks.asyncSeriesBailHook.tapPromise('AsyncSeriesBailPlugin2', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('triggered AsynsSeriesBailPlugin2 , args is: -- ', arg1, arg2)
      resolve()
    }, 1000)
  })
})
carInstance.hooks.asyncSeriesBailHook.tapPromise('AsyncSeriesBailPlugin3', (arg1, arg2) => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('triggered AsynsSeriesBailPlugin3 , args is: -- ', arg1, arg2)
      resolve(arg1)
    }, 1000)
  })
})
carInstance.asyncSeriesBailHookCallFunc(1, 2, (result) => {
  console.log('全部执行完毕', result)
  console.timeEnd('timer')
})

则输出结果是:

image.png

  • 小结 AsyncSeriesBailHook 异步串行钩子,在callback或者resolve中返回非undefined值时,则终止执行。

9. AsyncSeriesWaterfallHook

class Car {
    ,,,
    asyncSeriesWaterfallHookCallFunc(arg1, arg2, cb) {
        return this.hooks.asyncSeriesWaterfallHook.promise(arg1, arg2, cb)
    }
}
// 注册
carInstance.hooks.asyncSeriesWaterfallHook.tapAsync('AsyncSeriesWaterfallPlugin1', (arg1, arg2, cb) => {
  setTimeout(_ => {
    console.log('triggered AsyncSeriesWaterfallPlugin1', arg1, arg2)
    cb()
  }, 2000)
})
carInstance.hooks.asyncSeriesWaterfallHook.tapAsync('AsyncSeriesWaterfallPlugin2', (arg1, arg2, cb) => {
  setTimeout(_ => {
    console.log('triggered AsyncSeriesWaterfallPlugin2', arg1, arg2)
    cb()
  }, 1000)
})
// 调用
carInstance.asyncSeriesWaterfallHookCallFunc(1, 2, () => {
  console.log('全部执行完毕')
  console.timeEnd('timer')
})

输出结果:

image.png

  • 小结 AsyncSeriesWaterfallHook 异步瀑布钩子,无论监听的函数中是resolve 还是 reject 都会继续执行,直至执行完毕触发回调函数

Hook 总结

  • 同步: SyncHook,SyncBailHook, SyncLoopHook, SyncWaterfallHook

下图是对Tapable的一个总结

image.png

对于MultiHookHookMap都是辅助类,看下API调用就好

了解完Tapable,会结合去了解webpack内的插件价机制