Webpack插件机制之Tapable-源码解析

Webpack的成功之处,不只在于强大的打包构建能力,也在于它灵活的插件机制。javascript

Webpack本质上是一种事件流的机制,它的工做流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。java

在学习Webpack的时候,常常能够看到上述介绍。也就是说学Webpack的前提是要学习Tapable。才能更好的学习Webpack原理。node

1、Tapable

其实tapable的核心思路有点相似于node.js中的events,最基本的发布/订阅模式。webpack

const EventEmitter = require('events');
const myEmitter = new EventEmitter();

// 注册事件对应的监听函数
myEmitter.on('start', (params) => {
    console.log("输出", params)
});

// 触发事件 并传入参数
myEmitter.emit('start', '学习webpack工做流'); // 输出 学习webpack工做流
复制代码

2、tapable钩子介绍

首先,tapable提供的钩子有以下10个。 web

tapable钩子介绍

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesLoopHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");
复制代码

其次,全部钩子的用法简介,以下:(能够简单瞄一眼,就往下看吧)数组

序号 钩子名称 执行方式 使用要点
1 SyncHook 同步串行 不关心监听函数的返回值
2 SyncBailHook 同步串行 只要监听函数中有一个函数的返回值不为 undefined,则跳过剩下全部的逻辑
3 SyncWaterfallHook 同步串行 上一个监听函数的返回值能够传给下一个监听函数
4 SyncLoopHook 同步循环 当监听函数被触发的时候,若是该监听函数返回true时则这个监听函数会反复执行,若是返回 undefined 则表示退出循环
5 AsyncParallelHook 异步并发 不关心监听函数的返回值
6 AsyncParallelBailHook 异步并发 只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,而后执行这个被绑定的回调函数
7 AsyncSeriesHook 异步串行 不关心callback()的参数
8 AsyncSeriesBailHook 异步串行 callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数
9 AsyncSeriesWaterfallHook 异步串行 上一个监听函数的中的callback(err, data)的第二个参数,能够做为下一个监听函数的参数。
10 AsyncSeriesLoopHook 异步串行 能够触发handler循环调用。

3、上述Hook使用介绍

(1.1)SyncHook

同步串行,不关心监听函数的返回值。bash

咱们先来介绍最简单的SyncHook,其实每一个Hook都大同小异,懂一个其余的就很是好懂了。并发

const {SyncHook} = require("tapable");

//全部的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。
let queue = new SyncHook(['param1']); 

// 订阅tap 的第一个参数是用来标识订阅的函数的
queue.tap('event 1', function (param1) {
    console.log(param1, 1);
});

queue.tap('event 2', function (param1) {
    console.log(param1, 2);
});

queue.tap('event 3', function () {
    console.log(3);
});

// 发布的时候触发订阅的函数 同时传入参数
queue.call('hello');

// 控制台输出
/* hello 1 hello 2 3 */
复制代码

能够看到,这个钩子订阅的事件都是按顺序同步执行的。app

(1.2)SyncHook原理

简单模拟下原理。异步

class SyncHook{
    constructor(){
        this.taps = [];
    }

    // 订阅
    tap(name, fn){
        this.taps.push(fn);
    }

    // 发布
    call(){
        this.taps.forEach(tap => tap(...arguments));
    }
}
复制代码

(2.1)SyncBailHook

再来看下SyncBailHook的使用。

只要监听函数中有一个函数的返回值不为undefined,则跳过剩下全部的逻辑。

let queue = new SyncBailHook(['param1']); //全部的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。

// 订阅
queue.tap('event 1', function (param1) {// tap 的第一个参数是用来标识订阅的函数的
    console.log(param1, 1);
    return 1;
});

queue.tap('event 2', function (param1) {
    console.log(param1, 2);
});

queue.tap('event 3', function () {
    console.log(3);
});

// 发布
queue.call('hello', 'world');// 发布的时候触发订阅的函数 同时传入参数

// 控制台输出
/* hello 1 */
复制代码

能够看到,只要监听函数中有一个函数的返回值不为undefined,则跳过剩下全部的逻辑。

(2.2)SyncBailHook原理

简单模拟下原理。

class SyncBailHook {
    constructor() {
        this.taps = [];
    }

    // 订阅
    tap(name, fn) {
        this.taps.push(fn);
    }

    // 发布
    call() {
        for (let i = 0, l = this.taps.length; i < l; i++) {
            let tap = this.taps[i];
            let result = tap(...arguments);
            if (result) {
                break;
            }
        }
    }
}
复制代码

(3)SyncHook和SyncBailHook总结

上述2种的钩子的执行流程以下图所示:

经过这个 2个钩子的介绍,能够发现 tapable提供了各类各样的 hook来帮咱们管理事件是如何执行的。

tapable的核心功能就是控制一系列注册事件之间的执行流控制,好比我注册了三个事件,我能够但愿他们是并发的,或者是同步依次执行,又或者其中一个出错后,后面的事件就不执行了,这些功能均可以经过tapablehook实现。

就像起床、上班、吃早饭的关系同样,起床确定是优先的。可是吃饭和上班就不必定啦。万一要迟到了呢?可能就放弃早饭了!

吃饭

4、Tapable的源码解读

记住重点,核心就是calltap两个方法。
记住重点,核心就是calltap两个方法。
记住重点,核心就是calltap两个方法。

那咱们来看下tapable源码的SyncHook是如何实现的,以下。仍是那句话,看完一个,其余的天然就懂啦。为了理解,源码均为缩减过的,去除了些非核心代码。

// node_modules/tapable/lib/SyncHook.js
const factory = new SyncHookCodeFactory();
// 继承基础Hook类
class SyncHook extends Hook {
    // 重写Hook的compile方法
    compile(options) {
        // 开发者订阅的事件传
        factory.setup(this, options);
        // 动态生成call方法
    	return factory.create(options);
    }
}
module.exports = SyncHook;
复制代码

核心代码很是简单,能够看到SyncHook就是继承了Hook基础类。并重写了compile方法。

首先来看下Hook基础类的tap方法。能够看到每次调用tap,就是收集当前hook实例全部订阅的事件到taps数组。

// node_modules/tapable/lib/Hook.js
// 订阅
tap(options, fn) {
    // 同步 整理配置项
    options = Object.assign({ type: "sync", fn: fn }, options);
    // 将订阅的事件存储在taps里面
    this._insert(options);
}

_insert(item) {
    // 将item 推动 this.taps
    this.taps[i] = item;
}
复制代码

而后来看下Hook基础类的call方法是如何实现的。

// node_modules/tapable/lib/Hook.js
class Hook {
    constructor(args) {
    	this.taps = [];
    	this.call = this._call;
    }

    compile(options) {
    	// 继承类必须重写compile
    	throw new Error("Abstract: should be overriden");
    }
    
    // 执行compile生成call方法
    _createCall(type) {
    	return this.compile({
            taps: this.taps,
    		// ...等参数
    	});
    }
}

// 动态生成call方法
function createCompileDelegate(name, type) {
    return function lazyCompileHook(...args) {
    	// 创造call等函数
    	this[name] = this._createCall(type);
    	// 执行触发call等函数
    	return this[name](...args);
    };
}

// 定义_call方法
Object.defineProperties(Hook.prototype, {
    _call: {
    	value: createCompileDelegate("call", "sync"),
    	configurable: true,
    	writable: true
    },
});
复制代码

经过上述代码,咱们能够发现,call方法到底是什么,是经过重写的compile方法生成出来的。那咱们再看下compile方法究竟作了什么。

先来看下SyncHook的所有代码。

// node_modules/tapable/lib/SyncHook.js
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

// 继承工厂类
class SyncHookCodeFactory extends HookCodeFactory {
    // call方法个性化定制
    content({ onError, onDone, rethrowIfPossible }) {
    	return this.callTapsSeries({
            onError: (i, err) => onError(err),
            onDone,
            rethrowIfPossible
    	});
    }
}

const factory = new SyncHookCodeFactory();

// 继承基础Hook类
class SyncHook extends Hook {
    // 重写Hook的compile方法
    compile(options) {
        // 开发者订阅的事件传
        factory.setup(this, options);
        // 动态生成call方法
    	return factory.create(options);
    }
}

module.exports = SyncHook;
复制代码

能够看到compile主要是执行factory的方法,而factorySyncHookCodeFactory的实例,继承了HookCodeFactory类,而后factory实例调用了setup方法。

setup就是将taps中订阅的事件方法统一给了this._x;

// node_modules/tapable/lib/HookCodeFactory.js
setup(instance, options) {
    // 将taps里的全部fn 赋值给 _x
    instance._x = options.taps.map(t => t.fn);
}
复制代码

而后再看下factory实例调用的create方法。

// node_modules/tapable/lib/HookCodeFactory.js
create(options) {
    this.init(options);
    let fn;
    switch (this.options.type) {
        case "sync":
            fn = new Function(
                // 参数
                this.args(),
                // 函数体
                '"use strict";\n' +
                // 获取一些须要的变量
                this.header() +
                // 事件运行逻辑
                this.content({
                    onError: err => `throw ${err};\n`,
                    onResult: result => `return ${result};\n`,
                    resultReturns: true,
                    onDone: () => "",
                    rethrowIfPossible: true
                })
        );
        break;
    }
}
复制代码

create会将传进来的全部事件,进行组装。最终生成call方法。 以下就是咱们此次的案例最终生成的call方法。

function anonymous(param1) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(param1);
    var _fn1 = _x[1];
    _fn1(param1);
    var _fn2 = _x[2];
    _fn2(param1);
}
复制代码

若是你订阅了5个事件,上述代码就会变成5个函数的依次执行。以及参数必须是建立hook实例就声明好的。不然tap事件传的参数是无用的~

以上代码仍是简写了不少,你们能够直接去看下源码,很是精简好理解。给做者大大点赞。👍

总结一下,核心就是calltap两个方法。其实还有tapAsync等...可是原理都是同样的。tap收集订阅的事件,触发call方法时根据hook的种类动态生成对应的执行体。以下图,其余hook的实现也是同理。

Hook设计原理

5、Tapable在Webpack中的应用

Webpack的流程能够分为如下三大阶段:

执行webpack时,会生成一个compiler实例。

// node_modules/webpack/lib/webpack.js
const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");

const webpack = (options, callback) => {
	// ...省略了多余代码...
    let compiler;
    if (typeof options === "object") {
    	compiler = new Compiler(options.context);
    } else {
    	throw new Error("Invalid argument: options");
    }
})
复制代码

咱们发现Compiler是继承了Tapable的。同时发现webpack的生命周期hooks都是各类各样的钩子。

// node_modules/webpack/lib/Compiler.js
class Compiler extends Tapable {
    constructor(context) {
    super();
        this.hooks = {
            /** @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<string, Buffer>} */
            assetEmitted: new AsyncSeriesHook(["file", "content"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            afterEmit: new AsyncSeriesHook(["compilation"]),
        
            // ....等等等不少 你们看下源码吧.... 不看也没有关系
        }
    }
}
复制代码

而后在初始化webpack的配置过程当中,会循环咱们配置的以及webpack默认的全部插件也就是plugin

// 订阅在options中的全部插件
if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
            plugin.call(compiler, compiler);
        } else {
            plugin.apply(compiler);
        }
    }
}
复制代码

这个过程,会把plugin中全部tap事件收集到每一个生命周期的hook中。 最后根据每一个hook执行call方法的顺序(也就是生命周期)。就能够把全部plugin执行了。

举个例子,下面是咱们常用的热更新插件代码,它订阅了additionalPasshook

热更新插件
这也就是 webpack它工做流程能将各个插件 plugin串联起来的缘由,而实现这一切的核心就是 Tapable

6、吐个槽

虽然插件化设计很灵活,咱们能够写插件操做webpack的整个生命周期。可是也发现插件化设计带来的一些问题,就是阅读源码很是很差的体验:

(1)联系松散。使用tapable钩子相似事件监听模式,虽然能有效解耦,但钩子的注册与调用几乎没有联系。

(2)看到源码里一个模块提供了几个钩子,但并不知道,在什么时候、何地该钩子会被调用,又在什么时候、何地钩子上被注册了哪些方法。这些以往都是须要经过在代码库中搜索关键词来解决。

(3)钩子数量众多。webpack内部的钩子很是多,数量达到了180+

参考连接

本篇文主要是讲原理,理解tapable。其余的钩子的使用,能够看这篇文章。

相关文章
相关标签/搜索