umi源码-06执行插件和插件集

378 阅读6分钟

从这个阶段开始,代码中大量用到了 applyPlugins 这个方法,因此我们有必要搞清楚这个方法的具体细节。

register和registerMethod注册

先看一段官网描述 plugin-api#applyplugins 取得 register() 注册的 hooks 执行后的数据,这是一个异步函数,因此它返回的将是一个 Promise。这个方法的例子和详解见 register api。因此这个方法调用的时候,必须有 register注册的对应 hooks。 根据上一篇的介绍,我们知道 register有两种方式可以触发。 第一是通过registerMethod隐式注册,类似 api.registerMethod,传入函数名name,如果不传fn,最终还是会注册一个 register 方法 。 第二是通过插件扩展的方法显示调用,类似 api.register,传入函数名和对应的fn。 代码我们这里再贴一遍,方便理解。重点看 第28行和29行。这两行做了两件事,把函数放在 this.service.hooks = []数组中,实例化hooks, 暂存对应的函数名name和函数体fn,以及其他用于优化的 before stage字段。

registerMethod(opts: { name: string; fn?: Function }) {
  assert(
    !this.service.pluginMethods[opts.name],
    `api.registerMethod() failed, method ${opts.name} is already exist.`,
  );
  this.service.pluginMethods[opts.name] = {
    plugin: this.plugin,
    fn:
      opts.fn ||
      // 这里不能用 arrow function,this 需指向执行此方法的 PluginAPI
      // 否则 pluginId 会不会,导致不能正确 skip plugin
      function (fn: Function | Object) {
        // @ts-ignore
        this.register({
          key: opts.name,
          ...(lodash.isPlainObject(fn) ? (fn as any) : { fn }),
        });
      },
  };
}


register(opts: Omit<IHookOpts, 'plugin'>) {
  assert(
    this.service.stage <= ServiceStage.initPlugins,
    'api.register() should not be called after plugin register stage.',
  );
  this.service.hooks[opts.key] ||= [];
  this.service.hooks[opts.key].push(
    new Hook({ ...opts, plugin: this.plugin }),
  );
}

export class Hook {
  plugin: Plugin;
  key: string;
  fn: Function;
  before?: string;
  stage?: number;
  constructor(opts: IOpts) {
    assert(
      opts.key && opts.fn,
      `Invalid hook ${opts}, key and fn must supplied.`,
    );
    this.plugin = opts.plugin;
    this.key = opts.key;
    this.fn = opts.fn;
    this.before = opts.before;
    this.stage = opts.stage || 0;
  }
}

applyPlugins 与 tabable

注册方法已经完成了,我们需要在合适的时机执行函数,这时候就用到了 applyPlugins,这个方法用到了 webpack的插件机制,下面我们先把umi用到的webpack插件做一个简单的说明,方便后续理解。

tapable

webpack的插件系统本质是基于一个叫 tapable的库实现的,tabable有9个hooks,所有hooks都继承自Hook这个 class。umi中用到了其中的两个,分别是AsyncSeriesWaterfallHookSyncWaterfallHookimage.png Tapable 提供了一系列事件的发布订阅 API ,通过 Tapable我们可以注册事件,从而在不同时机去触发注册的事件进行执行。 在 Tapable 中所有注册的事件可以分为同步、异步两种执行方式,正如名称表述的那样:

  • 同步表示注册的事件函数会同步进行执行。

  • 异步表示注册的事件函数会异步进行执行。

  • 针对同步钩子来 tap 方法是唯一的注册事件的方法,通过 call 方法触发同步钩子的执行。

  • 异步钩子可以通过 tap、tapAsync、tapPromise三种方式来注册,同时可以通过对应的 call、callAsync、promise 三种方式来触发注册的函数。

同时异步钩子可以分为:

  • 异步串行钩子( AsyncSeries ):可以被串联(连续按照顺序调用)执行的异步钩子函数。
  • 异步并行钩子( AsyncParallel ):可以被并联(并发调用)执行的异步钩子函数。

更多细节可以参考这篇文章 参考资料

简单总结 实例化hook: const someHook = new Sync/AsyncXXXHook(['params']) 注册函数: someHook.tap /someHook.tabAsync/ someHook.tapPromise 执行函数 someHook.call/ someHook.callAsync /someHook.promise 上面总结的三步,会在umi中有所体现。下面我们来看一下 umi 中用到的两个2个hooks,具体看使用方法。

AsyncSeriesWaterfallHook

AsyncSeriesWaterfallHook 异步串行瀑布钩子:瀑布类型的钩子会在注册的事件执行时将事件函数执行非 undefined 的返回值传递给接下来的事件函数作为参数。

先看一个add的例子

const { SyncWaterfallHook, AsyncSeriesWaterfallHook } = require("tapable");

const hooks = [
  {
    name: 'fn1',
    args: '1',
    fn(params) {
      return params;
    }
  },
  {
    name: 'fn2',
    args: '2',
    fn(params) {
      return params;
    }
  }
]
// 第一步:实例化
const tAdd = new AsyncSeriesWaterfallHook(['memo']);
// 第二步:遍历注册
for (const hook of hooks) {
  tAdd.tapPromise(
    {
      name: hook.name,
    },
    async (memo) => {
      // 获取hook对应注册的函数 fn,并传入参数
      const items = await hook.fn(hook.args)
      const res = memo.concat(items)
      console.log(`${hook.args}==memo`,memo)
      console.log(`${hook.args}==items`,items)
      console.log(`${hook.args}==res`,res)
      return res;
    },
  );
}

// 第三步:执行注册的所有promise病传出初始值
tAdd.promise([123]).then(res => {
  console.log(res, 'final:promise===')
});

// 第一次循环
// 1==memo [ 123 ]             // tAdd.promise的初始值
// 1==items 1                  //  hooks中第一个函数的返回值
// 1==res [ 123, '1' ]         // 初始值和第一个函数返回值结合在一起

// 第二次循环
// 2==memo [ 123, '1' ]       // 第一次循环的返回值
// 2==items 2                 // hooks中第二个函数的返回值
// 2==res [ 123, '1', '2' ]   // 第一次循环和第二个函数返回值结合在一起

// 最终结果, 所有循环结束,初始值和所有函数返回值结合在一起
// [ 123, '1', '2' ] final:promise=== 

再看一个modify的例子

const { SyncWaterfallHook, AsyncSeriesWaterfallHook } = require("tapable");

const hooks = [
  {
    name: "fn1",
    args: "1",
    fn(memo, args) {
      memo.fn1 = args;
      return memo;
    },
  },
  {
    name: "fn2",
    args: "2",
    fn(memo, args) {
      memo.fn2 = args;
      return memo;
    },
  },
];

// 第一步:实例化
const tModify = new AsyncSeriesWaterfallHook(["memo"]);
// 第二步:遍历注册
for (const hook of hooks) {
  tModify.tapPromise(
    {
      name: hook.name,
    },
    async (memo) => {
      const ret = await hook.fn(memo, hook.args);
      console.log(`${hook.args}==memo`, memo);
      console.log(`${hook.args}==ret`, ret);
      return ret;
    }
  );
}
// 第三步:执行注册的所有promise病传出初始值
tModify.promise({ umi: "initialValue" }).then((res) => {
  console.log(res, 'final:promise===')
});

// 每次执行 fn1或者fn2 我们都修改了 memo的值,在memo上添加了属性 fn1 和 fn2。
// 1==memo { umi: 'initialValue', fn1: '1' }
// 1==ret { umi: 'initialValue', fn1: '1' }
// 2==memo { umi: 'initialValue', fn1: '1', fn2: '2' }
// 2==ret { umi: 'initialValue', fn1: '1', fn2: '2' }
// { umi: 'initialValue', fn1: '1', fn2: '2' } final:promise===


SyncWaterfallHook

SyncWaterfallHook 瀑布钩子会将上一个函数的返回值传递给下一个函数作为参数: umi中用到这个方法,只是为了注册和执行函数,没有返回值,也不修改参数,因此不需要详细解释

const { SyncWaterfallHook } = require('tapable');

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

// 注册事件
hook.tap('flag1', (arg1, arg2, arg3) => {
  console.log('flag1:', arg1, arg2, arg3);
  // 存在返回值 修改flag2函数的实参
  return 'github';
});

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

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

// 调用事件并传递执行参数,也可以通过 promise 调用
hook.call('19Qingfeng', 'wang', 'haoyu');
// 输出结果
flag1: 19Qingfeng wang haoyu
flag2: github wang haoyu
flag3: github wang haoyu

applyPlugins执行注册的方法

有了上面的tapable做基础,这个方法就很好理解了。 看代码发现,这里写了三次applyPlugins,这里用到了函数重载。

函数重载: 函数项名称相同 但输入输出类型或个数不同的子程序,它可以简单地称为一个单独功能可以执行多项任务的能力。 TypeScript 的函数重载: 为同一个函数提供多个函数类型定义来进行函数重载,目的是重载的函数在调用的时候会进行正确的类型检查。

applyPlugins分三类,

  • on开头的函数,表示一个事件 event,参数是applyPlugins args的值, 不需要有返回值
  • modify开头的函数,会按照hook顺序执行hook,后面的hook可以修改前面的值,并返回值
  • add开头的函数,会按照hook顺序把所有返回值拼接为一个数组,后面的hook可以用前面的返回值

具体细节和解释请看代码注释

// 定义函数的入参和返回值类型,async 时返回 initialValue 或者 T
applyPlugins<T>(opts: {
  key: string;
  type?: ApplyPluginsType.event;
  initialValue?: any;
  args?: any;
  sync: true;
}): typeof opts.initialValue | T;
// 定义函数的入参和返回值类型,非 async 时返回 Promise类型的
applyPlugins<T>(opts: {
  key: string;
  type?: ApplyPluginsType;
  initialValue?: any;
  args?: any;
}): Promise<typeof opts.initialValue | T>;

// 实现函数
applyPlugins<T>(opts: {
  key: string;
  type?: ApplyPluginsType;
  initialValue?: any;
  args?: any;
  sync?: boolean;
}): Promise<typeof opts.initialValue | T> | (typeof opts.initialValue | T) {
  const hooks = this.hooks[opts.key] || [];
  let type = opts.type;
  // 判断类型, on modify add 
  if (!type) {
    if (opts.key.startsWith('on')) {
      type = ApplyPluginsType.event;
    } else if (opts.key.startsWith('modify')) {
      type = ApplyPluginsType.modify;
    } else if (opts.key.startsWith('add')) {
      type = ApplyPluginsType.add;
    } else {
      throw new Error(
        `Invalid applyPlugins arguments, type must be supplied for key ${opts.key}.`,
      );
    }
  }
  switch (type) {
    case ApplyPluginsType.add:
      assert(
        !('initialValue' in opts) || Array.isArray(opts.initialValue),
        `applyPlugins failed, opts.initialValue must be Array if opts.type is add.`,
      );
      // add模式下 new 注册 hook,
      const tAdd = new AsyncSeriesWaterfallHook(['memo']);
      // 遍历所有hook
      for (const hook of hooks) {
        if (!this.isPluginEnable(hook)) continue;
        // 通过 tapPromise 注册hook,注意这里是批量注册事件,
        tAdd.tapPromise(
          {
            name: hook.plugin.key,
            stage: hook.stage || 0,
            before: hook.before,
          },
          async (memo: any) => {
            const dateStart = new Date();
            // 获取hook对应注册的函数 fn,并传入参数
            const items = await hook.fn(opts.args);
            hook.plugin.time.hooks[opts.key] ||= [];
            hook.plugin.time.hooks[opts.key].push(
              new Date().getTime() - dateStart.getTime(),
            );
            // add 模式下 拼接所有的值,并返回给下一个hook
            return memo.concat(items);
          },
        );
      }
      // 通过 promise执行hook, 参数为 initialValue。这里是批量执行前面注册的所有tapPromise
      // 如果没有hooks,即没有tapPromise, tAdd.promise也是会执行的
      return tAdd.promise(opts.initialValue || []) as Promise<T>;
    case ApplyPluginsType.modify:
      const tModify = new AsyncSeriesWaterfallHook(['memo']);
      for (const hook of hooks) {
        if (!this.isPluginEnable(hook)) continue;
        tModify.tapPromise(
          {
            name: hook.plugin.key,
            stage: hook.stage || 0,
            before: hook.before,
          },
          async (memo: any) => {
            const dateStart = new Date();
            // 返回值是当前执行的函数结果
            const ret = await hook.fn(memo, opts.args);
            hook.plugin.time.hooks[opts.key] ||= [];
            hook.plugin.time.hooks[opts.key].push(
              new Date().getTime() - dateStart.getTime(),
            );
            // 这里是管道式的调用
            // 当有多个modify的 hook时,前面的执行结果是后面的入参。
            return ret;
          },
        );
      }
      return tModify.promise(opts.initialValue) as Promise<T>;
    case ApplyPluginsType.event:
      if (opts.sync) {
        const tEvent = new SyncWaterfallHook(['_']);
        hooks.forEach((hook) => {
          if (this.isPluginEnable(hook)) {
            tEvent.tap(
              {
                name: hook.plugin.key,
                stage: hook.stage || 0,
                before: hook.before,
              },
              () => {
                  // 没有返回值
                const dateStart = new Date();
                hook.fn(opts.args);
                hook.plugin.time.hooks[opts.key] ||= [];
                hook.plugin.time.hooks[opts.key].push(
                  new Date().getTime() - dateStart.getTime(),
                );
              },
            );
          }
        });


        return tEvent.call(1) as T;
      }


      const tEvent = new AsyncSeriesWaterfallHook(['_']);
      for (const hook of hooks) {
        if (!this.isPluginEnable(hook)) continue;
        tEvent.tapPromise(
          {
            name: hook.plugin.key,
            stage: hook.stage || 0,
            before: hook.before,
          },
          async () => {
            const dateStart = new Date();
            await hook.fn(opts.args);
            hook.plugin.time.hooks[opts.key] ||= [];
            hook.plugin.time.hooks[opts.key].push(
              new Date().getTime() - dateStart.getTime(),
            );
          },
        );
      }
      return tEvent.promise(1) as Promise<T>;
    default:
      throw new Error(
        `applyPlugins failed, type is not defined or is not matched, got ${opts.type}.`,
      );
  }
}
  • api.ApplyPluginsType.add applyPlugins 将按照 hook 顺序来将它们的返回值拼接成一个数组。此时 fn 需要有返回值,fn 将获取 applyPlugins 的参数 args 来作为自己的参数。applyPlugins 的 initialValue 必须是一个数组,它的默认值是空数组。当 key 以 'add' 开头且没有显式地声明 type 时,applyPlugins 会默认按此类型执行。
  • api.ApplyPluginsType.modify applyPlugins 将按照 hook 顺序来依次更改 applyPlugins 接收的 initialValue, 因此此时 initialValue 是必须的 。此时 fn 需要接收一个 memo 作为自己的第一个参数,而将会把 applyPlugins 的参数 args 来作为自己的第二个参数。memo 是前面一系列 hook 修改 initialValue 后的结果, fn 需要返回修改后的memo 。当 key 以 'modify' 开头且没有显式地声明 type 时,applyPlugins 会默认按此类型执行。
  • api.ApplyPluginsType.event applyPlugins 将按照 hook 顺序来依次执行。此时不用传入 initialValue 。fn 不需要有返回值,并且将会把 applyPlugins 的参数 args 来作为自己的参数。当 key 以 'on' 开头且没有显式地声明 type 时,applyPlugins 会默认按此类型执行

一个例子 registerMethod

// 注册方法,
api.registerMethod({
  name: 'addSomePage'
});
// 执行方法
// 通过这种方法注册的函数是不会进入  for (const hook of hooks) { 循环的,
// 因为 this.hooks[opts.key]  没有对应的fn,
// 所以只会执行 tAdd.promise(opts.initialValue || []))
api
  .applyPlugins({
    key: 'addSomePage',
    type: api.ApplyPluginsType.add,
    initialValue: ['init-value'],
    args: 'args',   // 没有fn 所有 args其实没什么用
  })
  .then((res) => {
    // 这里的res其实就是 initialValue
    console.log(res);  // 'init-value'
  });

一个例子 register

// 注册,此处有fn
api.register({
  key: 'addSomeRegister',
  fn(params) {
    // applyPlugins 触发函数执行
    // params实参就是 args实参
    console.log(params); // 'args实参'
    return ['register'];
  },
});

// 执行
// 通过这种方法注册的函数是会进入  for (const hook of hooks) { 循环的,
// 因为 this.hooks[opts.key]  有对应的fn,
api
  .applyPlugins({
    key: 'addSomeRegister',
    type: api.ApplyPluginsType.add,
    initialValue: ['initialValue'],
    args: 'args实参',
  })
  .then((res) => {
    // 初始值 initialValue和 fn的返回值拼起来
    console.log(res); // [ 'initialValue', 'register' ]
  });

两种注册方法的差异分析

  • register 注册方法用的, 传入函数名和对应的fn,注册到 this.service.hooks 中, 可以通过 api.applyPlugins 调用
  • registerMethod 注册方法用,需要key,可以有函数fn可以没有fn
    • 有fn时,注册到 this.service.pluginMethods 中,可以通过 api.xxx
    • 没有fn时,会注册到 this.service.pluginMethods 中,也会注册到 this.service.hooks 中。因此可以通过 api.xxx 调用,也可以通过api.applyPlugins调用。