深入webpack-tapable篇

打算写一些webpack相关的文章, 今天就先打个样

引子

说到webpack, 都知道是一个打包工具, 从一个入口开始, 遍历所有依赖, 并把所有文件都打包成一个js文件, webpack在整个构建过程中需要做一系列的工作, 那么它对这些复杂的工作是怎么组织的呢?
当我们去看webpack源码的时候, 会发现webpack在初始化的时候调用了大量的没用过的plugin来给compiler提供各种能力, 让人眼花缭乱的plugin,就这么一个个的挂载到compiler里面, 完全看不懂到底干了点啥子, 所以让我们先看看plugin到底是个啥

plugin

先让我们看一个webpack plugin 简单到不能再简单的例子

1
2
3
4
5
6
7
8
9
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, compilation => {
console.log('webpack 构建过程开始!');
});
}
}

调用:

1
2
3
4
5
6
{ 
...
plugins: [
new ConsoleLogOnBuildWebpackPlugin(),
]
}

代码很简单, 所有插件都是一个class, 插件传入配置的时候会被实例化, 而webpack在注册plugin的时候会调用这个实例的apply方法, 并把这次构建的compiler作为参数传入,
这个compiler会在整个构建周期中存在, 那么这句到底是什么含义?

1
2
3
compiler.hooks.run.tap(pluginName, compilation => {
console.log('webpack 构建过程开始!');
});

其实从代码上可以看到, 这段代码在compiler的钩子run上挂了一个插件,当这个钩子被调用的时候, 我们传入的回调函数将会被执行console.log('webpack 构建过程开始!');
那么我们看到的个hooks到底是什么东西? 让我们看一下compiler的源码(注掉不需关注的代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const {
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
...
class Compiler {
/**
* @param {string} context the compilation path
*/
constructor(context) {
this.hooks = Object.freeze({
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
afterDone: new SyncHook(["stats"]),
additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]), // <------------- 上面调用的run在这里!!
emit: new AsyncSeriesHook(["compilation"]),
assetEmitted: new AsyncSeriesHook(["file", "info"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
thisCompilation: new SyncHook(["compilation", "params"]),
compilation: new SyncHook(["compilation", "params"]),
normalModuleFactory: new SyncHook(["normalModuleFactory"]),
...
});
...
}

可以发现Compiler类的所有hooks都是一个个不同类型的钩子的实例, 而这些钩子都来自于同一个库tapable.

tapable是什么

tapable是什么?
早先的webpack complier 是tapable的实例, 现在仅仅是tapable的调用者, 我也觉得这样更好, 把很多调用放到外部, 远比继承一个第三方库要好, 代码的调用更明显,不再那么隐晦

实际上webpack编译打包类似于一个流水线, 在流水线上有很多节点, 不同的节点暴露不同的属性和值, 你可以再不同的节点对这些属性和值左调整处理, 最后直到打包完成. 那么webpack流水线中的不同的节点我们成为不同的钩子(hook), 而这些钩子就是由tapable负责生成的.

今天先说说webpack的事件机制 tapable

tapable很像我们了解的发布订阅模式,我们先看看tapable是怎么做的

tapable 怎么用

1
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

这里是新声明了一个钩子, 大家留意, SyncHook的实参传入一个数组为参数, 每一个数组item代表将来挂载这个钩子上的回调函数时能够接受到的参数数量. 即时之后我们再触发钩子时传入在多的实参, 也无法超过三个. 但是这里有一个疑问, 但是为什么要这么写? 为什么不能吧触发钩子传入的所有实参都同步给钩子的回调? 这样不是应该更加灵活么?这仅仅是一种类似注释的约定么?方便开发更快的时候钩子回调内的参数是什么? 我们这里留一手, 最后再讲

我们还是先来看看tapable里面有什么把!它会导出多种钩子类型, 从大的类型里能分为SyncHook, AsyncSeriesHook, AsyncParalleHook这两种, 顾名思义, 一种钩子的所有回调都是同步的, 另两种则是异步的. 两个异步钩子一种是顺序执行,也就是一个接一个, 另一个则是同步执行, 就是一次吧所有钩子回调都执行

多个钩子在调用上也有不同的讲究, 通常是根据上一个钩子回调的返回值有不同的处理, 规则有如下三款, 还有一款正在设计当中

  • basic (基本款)
  • waterfall (大瀑布款)
  • bail (保险款)
  • loop (自留款, todo…)

基本款就不多说了, 所有的回调依次调用, 大家好对一个个报数就可以了
大瀑布款, 其实所有回调依次调用, 不同的的房是, 每一个回调都能够接受到上一个回调的返回值, 就类似于咱们的击鼓传花啦
保险款, 顾名思义, 跟保险丝差不多, 再依次执行回调的时候, 如果有任何一个回调有返回值, 则这个钩子的执行将提前结束

同步钩子和异步钩子再加上我们刚才说的三款不懂得调用处理方式, 就把整个钩子的类型翻了一倍, 下面这些也就是tapable提供的所有种类钩子:

  • SyncHook
  • SyncBailHook
  • SyncWaterfallHook
  • SyncLoopHook
  • AsyncParallelHook
  • AsyncParallelBailHook
  • AsyncSeriesHook
  • AsyncSeriesBailHook
  • AsyncSeriesWaterfallHook

上面知道了钩子都叫什么, 也知道了为啥叫这个名字, 下面我们就来看看钩子应该怎么用, 该怎么触发

1
2
3
4
5
6
7
8
// 定义一个钩子
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

hook.tap('whatEver', (arg1, arg2, arg3) => {
console.log(arg1, arg2, arg3) // 1 2 3
})

hook.call(1, 2,3)

实际上第一个参数 可以是一个对象, 里面可以有个before, stage, 用来设置这个plugin插入的位置
是不是相继了一般的发布订阅模式? hook.tap是订阅, hook.call是发布

刚我们看的是最最最简单的SyncHook, 他只有一种挂在钩子回调的方法, 就是tap, 实际上不同的钩子类型, 它提供的挂载方式也不同, 分成三种

  • tap
  • tapPromise
  • tapAsync
    区别也挺简单的, tap就是同步挂载,挂载的回调函数自然是一个同步函数, tapPromise挂载的异步函数了, 要求他必须返回一个Promise, tabAsync挂载的也是一个异步函数, 函数中的形参callback代表着函数的结束, 类似expressnext.

tapable也有拦截器, 其实就是在每个钩子的不同状态触发前的一个控制函数, 让你对整个钩子有更多的控制能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
hook.intercept({
context: true, // 开启时, 可以再拦截函数的第一个参数拿到context对象, context对象也可以从所有的钩子回调中获得
// hook 触发时
call: (arg1, arg2, arg2) => {
...
},
// hook 注册时
register: (tapInfo) => {
return tapInfo;
}
// 有新的钩子回调挂进来时
tap: (tap) => {
...
}
})