Skip to content
On this page

低代码平台中 Core作为插拔式的核心,需要发布出各种事件来让用户在 plugin的基础上实现监听以及拓展。
Core中发布的事件大概可以分为一下几类

  • 需要根据事件的返回值来决定后面逻辑是否执行
  • 异步串行且需要上一个返回值作为下一个参数的入参

Tapable

tapable 是一个类似于 Node.js 中的 EventEmitter的库,但更专注于自定义事件的触发和处理。webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的形式存在。

其中基本用法这里不赘述,大部分文章都有介绍。下面介绍下各类钩子的特点

按照执行机制分类

Basic hook

这一类钩子仅仅只是注册然后执行,并不关心每个事件里面的返回值等等。就像简单的发布订阅
image.png

Bail hook

这类钩子会根据事件的返回值如果不为**undefined**来决定下一个事件是否执行
image.png

WaterFallHook

这类钩子会根据事件返回值如果不为undefined来作为下一个事件的第一个参数值。它只能将上一事件的返回值传递给下一个事件第一个参数

image.png

LoopHook

这类钩子会根据任意一个事件返回值如果是undefined则回到第一个事件中
image.png

按照同步/异步分类

  • 同步钩子

只能使用 tap注册,使用 call调用

  • 异步钩子

可以使用 tap /tapAsync/ tapPromise来注册,对应callAsync/promise来调用.

【注意】

  1. callAsync/ promise调用会执行 tap /tapAsync/ tapPromise 注册的事件
  2. 使用tap注册的事件没有回调参数 callback
  3. 异步钩子注册事件会有而外参数 callback且固定为callback(错误信息,返回值)

源码分析

先来看看看看用法以及调用结果

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

// 初始化同步钩子
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

// 注册事件
hook.tap("flag1", (arg1, arg2, arg3) => {
  console.log("flag1:", arg1, arg2, arg3);
  return "github";
});

hook.tap("flag2", (arg1, arg2, arg3) => {
  console.log("flag2:", arg1, arg2, arg3);
});

// 调用事件并传递执行参数
console.log(hook.call("ayomc", "fei", "haoyu"), "result");

// 打印如下
// flag1: ayomc fei haoyu
// flag2: ayomc fei haoyu
// undefined result

看起来很简单对吧,这段代码通过 SyncHook 创建了一个同步 Hook 的实例之后,然后通过 tap 方法注册了两个事件,最后通过 call 方法来调用。

实质上这段代码在调用 hook.call("ayomc", "fei", "haoyu") 时, Tapable 会动态编译出来这样一个函数:

js
function fn(arg1, arg2, arg3) {
  "use strict";
  var _context;
  var _x = this._x; // 这个_x 实际上就是我们注册的函数
  var _fn0 = _x[0];
  _fn0(arg1, arg2, arg3);
  var _fn1 = _x[1];
  _fn1(arg1, arg2, arg3);
}

这样看起来 tapable 就干了两件事

  1. 根据注册的信息动态生成一个 fn
  2. 然后 实例对象 调用这个 fn

实际上还就真干了这两件事,利用 HookCodeFactory 类根据传入参数动态生成 fn ,利用 Hook 类来调用这个动态生成的 fn

SyncHook

可看到源码中每个 hook 都是一个单独的文件

js
const Hook = require("./Hook");

// 这个是重点
const HookCodeFactory = require("./HookCodeFactory");

class SyncHookCodeFactory extends HookCodeFactory {
  content({ onError, onDone, rethrowIfPossible }) {
    // 这里可以自定义hook的各种情况下的回调,返回值
    return this.callTapsSeries({
      onError: (i, err) => onError(err),
      onDone,
      rethrowIfPossible,
    });
  }
}

const factory = new SyncHookCodeFactory();

const TAP_ASYNC = () => {
  throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
  throw new Error("tapPromise is not supported on a SyncHook");
};

const COMPILE = function (options) {
  // compile 是每个钩子的核心

  // 初始化信息 存储参数,比如回调函数中有几个参数
  factory.setup(this, options);
  // 创建函数字符串
  return factory.create(options);
};

function SyncHook(args = [], name = undefined) {
  const hook = new Hook(args, name);
  hook.constructor = SyncHook;
  hook.tapAsync = TAP_ASYNC;
  hook.tapPromise = TAP_PROMISE;
  hook.compile = COMPILE;
  return hook;
}

SyncHook.prototype = null;

module.exports = SyncHook;

函数一开始将参数传入 Hook 类中。然后又实例化了 Hook ,并给一些 API 添加调用提示,那就进入 Hook 探一探

js
class Hook {
  constructor(args = [], name = undefined) {
    // 保存调用回调的参数
    this._args = args;
    this.name = name;
    // 保存通过tap注册的内容
    this.taps = [];
    // 拦截器
    this.interceptors = [];

    this._x = undefined;
    this.tap = this.tap;
    // 动态编译的核心
    this.compile = this.compile;

    // this.tapAsync = this.tapAsync;
    // this.tapPromise = this.tapPromise;
    // this._callAsync = CALL_ASYNC_DELEGATE;
    // this.callAsync = CALL_ASYNC_DELEGATE;
    // this._promise = PROMISE_DELEGATE;
    // this.promise = PROMISE_DELEGATE;
  }

  compile(options) {
    throw new Error("Abstract: should be overridden");
  }
}

这里只需要搞清楚,在 new SyncHook(args)Tapable 内部究竟保存了哪些属性。

所谓 compile 方法正是编译我们最终生成的执行函数的入口方法,同时我们可以看到在 Hook 类中并没有实现 compile 方法,

这是因为不同类型的 Hook 最终编译出的执行函数是不同的形式,所以这里以一种抽象方法的方式将 compile 方法交给了子类进行实现。

compile 将会是我们重点要注意的方法

实现 tap 注册

js
const syncHook = new SyncHook(["args"], "cxk");

syncHook.tap("xxx", (args1) => {
  console.log(args1); //  rap
});

syncHook.call("rap");

class Hook {
  tap(options, fn) {
    this._tap("sync", options, fn);
  }
  _tap(type, options, fn) {
    if (typeof options === "string") {
      options = {
        name: options.trim(),
      };
    } else if (typeof options !== "object" || options === null) {
      throw new Error("Invalid tap options");
    }
    options = Object.assign({ type, fn }, options);
    // 注册拦截器
    options = this._runRegisterInterceptors(options);
    // 保存回调
    this._insert(options);
  }

  _insert(item) {
    this.taps.push(item);
  }
}

大家可能注意到 _tap 实际上可以接收一个 Object,它的类型大概是这样,我们拦截器章节再详细讲解他

实现 call 调用

上面了解 tapable 如何存储我们监听的回调,下面来分析下是如何调用的

js
const CALL_DELEGATE = function (...args) {
  this.call = this._createCall("sync");
  return this.call(...args);
};

class Hook {
  constructor() {
    // hook.call 调用方法
    this._call = CALL_DELEGATE;
    this.call = CALL_DELEGATE;
  }
  compile(options) {
    throw new Error("Abstract: should be overridden");
  }

  _createCall(type) {
    return this.compile({
      taps: this.taps,
      interceptors: this.interceptors,
      args: this._args,
      type: type,
    });
  }
}

当我们使用 call 的时候实际上还是调用的我们一开始的重写的 compile 方法。

js
class SyncHookCodeFactory extends HookCodeFactory {
  content({ onError, onDone, rethrowIfPossible }) {
    return this.callTapsSeries({
      onError: (i, err) => onError(err),
      onDone,
      rethrowIfPossible,
    });
  }
}
const factory = new SyncHookCodeFactory();

const COMPILE = function (options) {
  factory.setup(this, options);
  return factory.create(options);
};

这样子看来 tapable 的分工就很明确了 Hook 类负责创建上下文存储调用的信息而 HookCodeFactory 则是只需要关注我该如何去生成这个动态函数,然后利用上下文信息去调用。

js
class HookCodeFactory {
  setup(instance, options) {
    // 储存回调函数,方便后面循环函数个数
    instance._x = options.taps.map((t) => t.fn);
  }

  create(options) {
    this.init(options);
    let fn;
    switch (this.options.type) {
      case "sync":
        fn = new Function(
          this.args(),
          '"use strict";\n' +
            this.header() +
            this.contentWithInterceptors({
              onError: (err) => `throw ${err};\n`,
              onResult: (result) => `return ${result};\n`,
              resultReturns: true,
              onDone: () => "",
              rethrowIfPossible: true,
            })
        );
        break;
    }
    this.deinit();
    return fn;
  }
  init(options) {
    this.options = options;
    this._args = options.args.slice();
  }

  deinit() {
    this.options = undefined;
    this._args = undefined;
  }
  contentWithInterceptors(options) {
    // SyncHook 注册的content方法 调用 串行调用函数体
    //   content({ onError, onDone, rethrowIfPossible }) {
    // 	   return this.callTapsSeries({
    // 	  	onError: (i, err) => onError(err),
    // 	  	onDone,
    // 	  	rethrowIfPossible
    // 	 });
    //  }
    return this.content(options);
  }

  header() {
    let code = "";
    if (this.needContext()) {
      code += "var _context = {};\n";
    } else {
      code += "var _context;\n";
    }
    code += "var _x = this._x;\n";
    if (this.options.interceptors.length > 0) {
      code += "var _taps = this.taps;\n";
      code += "var _interceptors = this.interceptors;\n";
    }
    return code;
  }

  needContext() {
    for (const tap of this.options.taps) if (tap.context) return true;
    return false;
  }

  // 根据this._x生成整体函数内容
  callTapsSeries({ onDone }) {
    let code = "";
    let current = onDone;
    // 没有注册的事件则直接返回
    if (this.options.taps.length === 0) return onDone();
    // 遍历taps注册的函数 编译生成需要执行的函数
    for (let i = this.options.taps.length - 1; i >= 0; i--) {
      const done = current;
      // 一个一个创建对应的函数调用
      const content = this.callTap(i, {
        onDone: done,
      });
      current = () => content;
    }
    code += current();
    return code;
  }

  // 编译生成单个的事件函数并且调用 比如 fn1 = this._x[0]; fn1(...args)
  callTap(tapIndex, { onDone }) {
    let code = "";
    // 无论什么类型的都要通过下标先获得内容
    // 比如这一步生成 var _fn[1] = this._x[1]
    code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
    // 不同类型的调用方式不同
    // 生成调用代码 fn1(arg1,arg2,...)
    const tap = this.options.taps[tapIndex];
    switch (tap.type) {
      case "sync":
        code += `_fn${tapIndex}(${this.args()});\n`;
        break;
      // 其他类型不考虑
      default:
        break;
    }
    if (onDone) {
      code += onDone();
    }
    return code;
  }

  // 从this._x中获取函数内容 this._x[index]
  getTapFn(idx) {
    return `_x[${idx}]`;
  }
}

这部分代码看上非常绕,一下是本身的方法,一下又是调用父类重写的方法。Tapable 中恰恰利用这种设计方式组织代码从而更好的解耦各个模块。让每个 Hook 能够专注于做自己的事情。这是很值得我们学习以及借鉴的地方。

可以看到我们通过 create 这时候就能生成出一个根据 注册信息 以及 new Function 动态生成一个这样的函数

js
new Function(
  `arg1, arg2, arg3`,
  `
  "use strict";
  var _context;
  var _x = this._x; // 这个_x 实际上就是我们注册的函数
  var _fn0 = _x[0];
  _fn0(arg1, arg2, arg3);
  var _fn1 = _x[1];
  _fn1(arg1, arg2, arg3);
`
);

异步 hook

tapable 对异步的处理分为两种

  1. 回调函数式,通过 callAsync 调用, tapAsync 注册
  2. Promise 式, 通过 Promise 调用, tapPromise 注册

第一种注册调用,将会编译成下面这样

js
(function anonymous(arg1, arg2, arg3, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  function _next1() {
    var _fn2 = _x[2];
    _fn2(arg1, arg2, arg3, function (_err2) {
      if (_err2) {
        _callback(_err2);
      } else {
        _callback();
      }
    });
  }
  function _next0() {
    var _fn1 = _x[1];
    _fn1(arg1, arg2, arg3, function (_err1) {
      if (_err1) {
        _callback(_err1);
      } else {
        _next1();
      }
    });
  }
  var _fn0 = _x[0];
  _fn0(arg1, arg2, arg3, function (_err0) {
    if (_err0) {
      _callback(_err0);
    } else {
      _next0();
    }
  });
});

可以看到这种就类似于 Promise 还未发展起来时期的,回调函数式来控制异步,上一个函数必须调用 callback 才会执行下一个函数

而对于 Promise 式的调用来说我们也可以看看

js
(function anonymous(arg1, arg2, arg3, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  function _next0() {
    var _fn1 = _x[1];
    var _promise1 = _fn1(arg1, arg2, arg3);
    if (!_promise1 || !_promise1.then)
      throw new Error(
        "Tap function (tapPromise) did not return promise (returned " +
          _promise1 +
          ")"
      );
    _promise1.then(
      function (_result1) {
        _callback(null, arg1, arg2, arg3);
      },
      function (_err1) {
        if (_hasResult1) throw _err1;
        _callback(_err1);
      }
    );
  }
  var _fn0 = _x[0];
  var _hasResult0 = false;
  var _promise0 = _fn0(arg1, arg2, arg3);
  if (!_promise0 || !_promise0.then)
    throw new Error(
      "Tap function (tapPromise) did not return promise (returned " +
        _promise0 +
        ")"
    );
  _promise0.then(
    function (_result0) {
      _next0();
    },
    function (_err0) {
      _callback(_err0);
    }
  );
});

可选参数

上节我们提到注册 tap 的时候第一个参数实际上可以传入一个对象,它的类型大概是这样

ts
type Tap = TapOptions & {
  name: string;
};

type TapOptions = {
  before?: string;
  stage?: number;
};

before&stage

before

我们注册的回调是改变顺序的如果有 before 的话则会插入到这个 监听函数 name 的前面,没有则是插入到最后。

js

stage

stage 这个属性的类型是数字,数字越大事件回调执行的越晚,支持传入负数,不传时默认为 0.

INFO

如果同时使用 beforestage 时,优先会处理 before ,在满足 before 的条件之后才会进行 stage 的判断。 关于 beforestage 都可以修改事件回调函数的执行时间,但是不建议混用这两个属性。换句话说如果你选择在你的 hooks.tap 中使用 stage 的话就不要在出现 before ,反之亦然。