技术/工程化/Webpack
Github Tapable—Just a little module for plugins

Webpack看似复杂的插件机制,其核心在于Tapable库,该库类似Nodejs中的EventEmitter,可以用于控制钩子函数的订阅和发布,而且Tapable支持多种执行方式,主要包括:
1460000018997505.png


基于触发时间

  • 同步顺序执行,函数返回时执行下个函数
  • 异步顺序执行,函数最后一个参数作为回调
  • 并行执行,同时执行所有函数

基于返回值

  • 无返回值, 顺序执行所有函数
  • Bail,函数顺序执行,直到其中一个有返回值
  • 并行Bail: 所有函数同时执行,第一个返回的值作为结果
  • Waterfall,所有函数都接受上个函数的结果作为参数

[image:63AA80F6-EB67-4B58-9610-2EB865BDDDFD-1498-000042990C6C7AC9/1.png]
[image:50B298AB-71E4-49F9-B3FE-A952FF957EBB-2549-0000040CDF0494C8/167f458ac2b1e527.png]
[image:92837DA5-94DA-4129-AFF2-D5D81CC1AC26-2549-00000414170AD995/167f458d6ff8424f.png]

Tapable源码分析

Tapable暴露出许多钩子基类,方便在不同场景使用。

  1. const {
  2. SyncHook,
  3. SyncBailHook,
  4. SyncWaterfallHook,
  5. SyncLoopHook,
  6. AsyncParallelHook,
  7. AsyncParallelBailHook,
  8. AsyncSeriesHook,
  9. AsyncSeriesWaterfallHook
  10. } = require('tapable')

可以通过命令: npm install —save tapable安装tapable库

Hook基类

https://github.com/webpack/tapable/blob/master/lib/Hook.js

SyncHook实现

SyncHook继承自Hook基类:

  1. class SyncHook extends Hook {
  2. // 同步hook不允许异步钩子
  3. tapAsync() {}
  4. tapPromise() {}
  5. compile(options) {
  6. factory.setup(this, options);
  7. return factory.create(options);
  8. }
  9. }

初始化一个SyncHook实例:

  1. const hook = new SyncHook(['arg1', 'arg2'])

注册事件:

  1. hook.tab('start', function(arg1, arg2){})
  2. hook.tab('end', function(arg1, arg2){})

下面分析tap()方法:

// 处理参数
if (typeof options === 'string') options = { name: options };
if (typeof options !== 'object' || options === null) throw new Error('Invalid options!')
options = Object.assign({type: 'sync', fn: fn}, options)
if (typeof options.name !== 'string' || options.name === '') throw new Error('Missing tab name')
// 添加拦截器
options = this._runRegisterInterceptor(options);
// 注册事件
this._insert(options)

核心逻辑在于注册过程,下面分析insert()方法:

_insert(item) {
    this._resetCompilation();

    // 将事件排序后放入taps数组
    let before;
    if (typeof item.before === 'string') before = new Set([item.before]);
    else if (Array.isArray(item.before)) before = new Set(item.before]);

    let stage = 0;
    if (typeof item.stage === 'number') stage = item.stage;
    let i = this.taps.length;
    // 确定item插入位置
    while(i > 0) {
        i --;
        const x = this.taps[i];
        this.taps[i + 1] = x;
        const xStage = x.stage || 0;
        // 判断x是否为item的before元素,是则跳过不调整位置
        if (before) {
            if (before.has(x.name)) {
                before.delete(x.name);
                continue;
            }
            if (before.size > 0)  continue;
        }
        // 遍历元素的stage大于stage则继续调整顺序
        if (xStage > stage) continue;
        i ++;
        break;
    }
    this.taps[i] = item;
}

上述方法,将注册的事件按照stage顺序放入taps数组。

触发事件:

hook.call(1, 2)

call()方法继承自基类Hook

Object.defineProperties(Hook.prototype, {
    _call: {
        value: createCompileDelegate('call', 'sync'),
        configurable: true,
        writable: true
 },
 _promise: {}, // 'promise', 'promise'
 _callAsync: {}, // 'callAsync', 'async'
})

_callcreateCompileDelegate生成,返回的是一个lazyCompileHook,直到调用call才会编译出call函数。

createCompileDelegate则是调用_createCall_createCall调用compile,。

重点是compile方法,该方法负责返回最后的call函数,但是该方法必须由子类重写,即由子类决定返回哪种call函数。SyncHook中确实重写了该方法:

compile(options) {
    factory.setup(this, options);
    return factory.create(options);
}

其中factory由HookCodeFactory提供,负责根据Hook类型生成不同的代码,详细代码可查看 https://github.com/webpack/tapable/blob/master/lib/HookCodeFactory.js

具体生程省略,最终返回的函数内容如下:

"use strict";
function (options) {
  var _context;
  var _x = this._x;
  var _taps = this.taps;
  var _interterceptors = this.interceptors;
// 我们只有一个拦截器所以下面的只会生成一个
  _interceptors[0].call(options);

  var _tap0 = _taps[0];
  _interceptors[0].tap(_tap0);
  var _fn0 = _x[0];
  _fn0(options);
  var _tap1 = _taps[1];
  _interceptors[1].tap(_tap1);
  var _fn1 = _x[1];
  _fn1(options);
  var _tap2 = _taps[2];
  _interceptors[2].tap(_tap2);
  var _fn2 = _x[2];
  _fn2(options);
  var _tap3 = _taps[3];
  _interceptors[3].tap(_tap3);
  var _fn3 = _x[3];
  _fn3(options);
}

上面分析tapable核心的功能和实现方式,那么可以去研究一下,Webpack中的compiler和compilation是怎么基于tapable运行的。

Webpack中的compiler和compilation

Compiler对象

Compiler是webpack的主模块,可以实例一个compiler对象,代表当前的webpack环境,通过该实例的run方法可以创建一个compilation实例;另外,应用插件时,插件会接收compiler对象的引用从而访问webpack环境

Compiler实现:

class Compiler extends Tapable {

    constructor(context) {

        super();

        this.hooks = {

            /** @type {SyncBailHook<Compilation>} */

            shouldEmit: new SyncBailHook(["compilation"]),

            /** @type {AsyncSeriesHook<Stats>} */

            done: new AsyncSeriesHook(["stats"]),

            /** @type {AsyncSeriesHook<>} */

            additionalPass: new AsyncSeriesHook([]),

            /** @type {AsyncSeriesHook<Compiler>} */

            beforeRun: new AsyncSeriesHook(["compiler"]),

            /** @type {AsyncSeriesHook<Compiler>} */

            run: new AsyncSeriesHook(["compiler"]),

            /** @type {AsyncSeriesHook<Compilation>} */

            emit: new AsyncSeriesHook(["compilation"]),

            /** @type {AsyncSeriesHook<Compilation>} */

            afterEmit: new AsyncSeriesHook(["compilation"]),
    }
}}

定义一系列编译过程中的钩子,用于插件注册事件,如run事件,会在run()方法执行时调用this.hooks.run.callAsync(this, callback)来触发,另外在此之前会调用this.hooks.beforeRun.callAsync(this, callback)来触发beforeRun事件
[https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L308](https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L308)

this.hooks.beforeRun.callAsync(this, err => {
        if (err) return finalCallback(err);
        this.hooks.run.callAsync(this, err => {
            if (err) return finalCallback(err);
            this.readRecords(err => {
                if (err) return finalCallback(err);
                this.compile(onCompiled);
            });
        });
    });

Compiler对象的使用,则需要实现一个自定义插件,如:

function UglifyJsPlugin(options) { this.options = options}
module.exports = UglifyJsPlugin;

// 关键, 将插件注册到Compiler的生命周期钩子事件
UglifyJsPlugin.prototype.apply = function(compiler) {
    compilr.plugin('compliation', function(compilation) {
        compilation.plugin('build-module', callback(module));
        compilation.plugin('optimize', callback(module))
    })
}

Compilation对象

实例compiler执行run()方法后,开始webpack的工作流程,则会创建compilation对象,该对象代表了一次资源版本构建。compilation不仅负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法如buildModule, addEntry等,而且还存放着所有module,chunk,assets以及用来生成最后打包文件的template信息

Compilation实现:

class Compilation extends Tapable {
    constructor(compiler) {
        super();
        this.hooks = {
            buildModule: new SyncHook(['module']),
            rebuildModule: new SyncHook(['module']),
            addEntry: new SyncHook(['entry', 'name'])
            ...
        }
    }
}

Compilation对象同样继承自Tapable类,并定义了一系列的钩子,如addEntry事件,则会在执行addEntry()方法时调用this.hooks.addEntry.call(entry, name)

通过compiler和compilation可以看出,想控制编译过程,其实就是在不同时机注册各种不同功能的插件即可。
Compiler对象几个关键的事件节点:

  • compile,开始编译
  • make, 创建模块对象
  • build-module, 构建模块
  • after-compile,完成构建
  • seal,封装构建结果
  • emit,把各个chunk输出到结果文件
  • after-emit,完成输出

Compilation对象的几个关键事件节点:

  • add-entry, 添加入口文件
  • build-module,构建模块
  • seal, 封装模块
  • afterChunks,划分chunk完成
  • chunkAsset, 一个 chunk 中的一个资源被添加到编译中
‘before run’
  ‘run’
    compile:func//调用compile函数
        ‘before compile’
           ‘compile’//(1)compiler对象的第一阶段
               newCompilation:object//创建compilation对象
               ‘make’ //(2)compiler对象的第二阶段 
                    compilation.finish:func
                       “finish-modules”
                    compilation.seal
                         “seal”
                         “optimize”
                         “optimize-modules-basic”
                         “optimize-modules-advanced”
                         “optimize-modules”
                         “after-optimize-modules”//首先是优化模块
                         “optimize-chunks-basic”
                         “optimize-chunks”//然后是优化chunk
                         “optimize-chunks-advanced”
                         “after-optimize-chunks”
                         “optimize-tree”
                            “after-optimize-tree”
                            “should-record”
                            “revive-modules”
                            “optimize-module-order”
                            “advanced-optimize-module-order”
                            “before-module-ids”
                            “module-ids”//首先优化module-order,然后优化module-id
                            “optimize-module-ids”
                            “after-optimize-module-ids”
                            “revive-chunks”
                            “optimize-chunk-order”
                            “before-chunk-ids”//首先优化chunk-order,然后chunk-id
                            “optimize-chunk-ids”
                            “after-optimize-chunk-ids”
                            “record-modules”//record module然后record chunk
                            “record-chunks”
                            “before-hash”
                               compilation.createHash//func
                                 “chunk-hash”//webpack-md5-hash
                            “after-hash”
                            “record-hash”//before-hash/after-hash/record-hash
                            “before-module-assets”
                            “should-generate-chunk-assets”
                            “before-chunk-assets”
                            “additional-chunk-assets”
                            “record”
                            “additional-assets”
                                “optimize-chunk-assets”
                                   “after-optimize-chunk-assets”
                                   “optimize-assets”
                                      “after-optimize-assets”
                                      “need-additional-seal”
                                         unseal:func
                                           “unseal”
                                      “after-seal”
                    “after-compile”//(4)完成模块构建和编译过程(seal函数回调)    
    “emit”//(5)compile函数的回调,compiler开始输出assets,是改变assets最后机会
    “after-emit”//(6)文件产生完成

看一眼实际生成的Compiler对象和Compilation对象:

关于compiler和compilation众多的钩子及钩子类型参考官网说明:
compilation 钩子
compiler 钩子

参考文章:
[Segmentfault-Tapable文档]
[知乎-Tapable源码分析]
[Tapable-钩子使用和原理]