从这个阶段开始,代码中大量用到了 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中用到了其中的两个,分别是AsyncSeriesWaterfallHook 和 SyncWaterfallHook。
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调用。
- 有fn时,注册到