插件集是插件的集合,本质还是插件。因此,初始化插件集其实就是遍历插件集中的插件,然后初始化。初始化插件也是遍历所有插件,然后初始化。
初始化插件其实就是执行 new PluginAPI()
PluginAPI 的原理
Umi 会为每个插件赋予一个 PluginAPI 对象,这个对象引用了插件本身和 Umi 的 service。 Umi 为 PluginAPI 对象的 get() 方法进行了 proxy, 具体规则如下:
- pluginMethod: 如果 prop 是 Umi 所维护的 pluginMethods[] ( 通过 registerMethod() 注册的方法 )中的方法,则返回这个方法。
- service props: 如果 prop 是 serviceProps 数组中的属性(这些属性是 Umi 允许插件直接访问的属性),则返回 service 对应的属性。
- static props: 如果 prop 是参数 staticProps 数组中的属性(这些属性是静态变量,诸如一些类型定义和常量),则将其返回。
- 否则返回 api 的属性
因此,Umi 提供给插件的 api 绝大多数都是依靠 registerMethod() 来实现的,你可以直接使用我们的这些 api 快速地在插件中注册 hook。这也是 Umi 将框架和功能进行解耦的体现: Umi 的 service 只提供插件的管理功能,而 api 都依靠插件来提供。
注册插件可以获得的能力
new PluginAPI 获得的能力, 参考 umi 核心 API文档
- describe 描述插件
- registerCommand 注册命令
- registerGenerator
- register 为 api.applyPlugins 注册可供其使用的 hook
- registerMethod 往 api 上注册一个名为 'name' 的方法
- registerPresets 注册插件集
- registerPlugins 注册插件
- skipPlugins 声明哪些插件需要被禁用
service中获取的能力
- service获取的能力都放在了 pluginMethods,而pluginMethods 通过 registerMethod设置。通过下面代码一目了然。因此这里是用户可扩展的能力。实际上umi内部的扩展方法都是通过
registerMethod
实现的。具体参考 plugin-api#扩展方法。下一篇我们会继续讲这个方法。
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;
}
}
serviceProps获取的能力
都是一些变量,这些变量都在 service中设置过,可以直接用。因此sercice中的这些暴漏出来的变量也是所有插件通用的。我们可以基于这些变量做一些事情。
- appData
- applyPlugins
- args
- config
- cwd
- pkg
- pkgPath
- name
- paths
- userConfig
- env
- isPluginEnable
staticProps获取的能力
- ApplyPluginsType 包含 add、modify、event
- ConfigChangeType 包含 reload、regenerateTmpFiles
- EnableBy 包含 register、config
- ServiceStage 生命周期 包含uninitialized、init、initPresets、initPlugins、resolveConfig、collectAppData、onCheck、onStart、runCommand
- service 包含的就比较多了,基本就是 所以核心能力了,其中比较重要的事 applyPlugins
// 第一步:实例化插件
const pluginAPI = new PluginAPI({
plugin: opts.plugin,
service: this,
});
// 第二步:给插件添加 registerPresets方法
pluginAPI.registerPresets = pluginAPI.registerPresets.bind(
pluginAPI,
opts.presets || [],
);
// 第三步:给插件添加 registerPlugins 方法
pluginAPI.registerPlugins = pluginAPI.registerPlugins.bind(
pluginAPI,
opts.plugins,
);
// 插件熟悉劫持,其中 包含了前面三部之后的 pluginAPI 所以方法。
const proxyPluginAPI = PluginAPI.proxyPluginAPI({
service: this,
pluginAPI,
serviceProps: [
'appData',
'applyPlugins',
'args',
'config',
'cwd',
'pkg',
'pkgPath',
'name',
'paths',
'userConfig',
'env',
'isPluginEnable',
],
staticProps: {
ApplyPluginsType,
ConfigChangeType,
EnableBy,
ServiceStage,
service: this,
},
});
// 这里的 proxyPluginAPI 其实就是插件的入参实参,相当于给插件挂载了很多的方法供用户调用
let ret = await opts.plugin.apply()(proxyPluginAPI);
我们来看一个简单的插件, 插件中的 api 等价于 上面所说的 proxyPluginAPI。
import { IApi } from 'umi';
export default (api: IApi) => {
api.describe({
key: 'changeFavicon',
config: {
schema(joi) {
return joi.string();
},
},
enableBy: api.EnableBy.config
});
api.modifyConfig((memo)=>{
memo.favicon = api.userConfig.changeFavicon;
return memo;
});
};
下面在贴一下 proxy的写法,知道proxy的应该都明白,这里简单看一下
- 首先去pluginMethods找对应的方法或者值,找到了就返回
- 然后去 serviceProps 找对应的方法或者值,找到了就返回
- 再然后去 staticProps 找对应的方法或者值,找到了就返回
- 最后才是target(pluginAPI)找对应的方法或者值,找到了就返回
static proxyPluginAPI(opts: {
pluginAPI: PluginAPI;
service: Service;
serviceProps: string[];
staticProps: Record<string, any>;
}) {
return new Proxy(opts.pluginAPI, {
get: (target, prop: string) => {
if (opts.service.pluginMethods[prop]) {
return opts.service.pluginMethods[prop].fn;
}
if (opts.serviceProps.includes(prop)) {
// @ts-ignore
const serviceProp = opts.service[prop];
return typeof serviceProp === 'function'
? serviceProp.bind(opts.service)
: serviceProp;
}
if (prop in opts.staticProps) {
return opts.staticProps[prop];
}
// @ts-ignore
return target[prop];
},
});
}
插件中可以嵌套插件
插件可以返回插件或者插件集,看一个返回插件和插件集的例子
import { IApi } from 'umi';
export default (api: IApi) => {
return {
plugins: ['./plugin_foo','./plugin_bar'],
presets: ['./preset_foo']
}
};
针对这种情况umi怎么处理?请看下面的代码。 其实也很简单,遍历插件或者插件集,通过 new Plugin 实例化
if (ret?.presets) {
ret.presets = ret.presets.map(
(preset: string) =>
new Plugin({
path: preset,
type: PluginType.preset,
cwd: this.cwd,
}),
);
}
if (ret?.plugins) {
ret.plugins = ret.plugins.map(
(plugin: string) =>
new Plugin({
path: plugin,
type: PluginType.plugin,
cwd: this.cwd,
}),
);
}
插件的描述信息
每个插件应当有描述信息,在插件注册阶段( initPresets or initPlugins stage )执行,用于描述插件或者插件集的 key、配置信息和启用方式等。当然,如果你没有写描述信息,也不会报错。 一个描述信息大概这样
api.describe({
key: 'foo',
config: {
default: 'Hello, Umi!',
schema(joi){
return joi.string();
},
onChange: api.ConfigCHangeType.regenerateTmpFiles,
},
enableBy: api.EnableBy.config,
})
这个 describe方法其实就是在插件注册的时候执行的,上面我们已经提过过了,通过 new PluginAPI获得了 describe 的能力。这个方法其实也很简单,就是把key、config等参数放入实例化的插件中
describe(opts: {
key?: string;
config?: IPluginConfig;
enableBy?:
| EnableBy
| ((enableByOpts: { userConfig: any; env: Env }) => boolean);
}) {
// default 值 + 配置开启冲突,会导致就算用户没有配 key,插件也会生效
if (opts.enableBy === EnableBy.config && opts.config?.default) {
throw new Error(
`[plugin: ${this.plugin.id}] The config.default is not allowed when enableBy is EnableBy.config.`,
);
}
this.plugin.merge(opts);
}
那么这个描述信息有什么用呢?下面我们看插件注册阶段对描述信息的处理
// collect configSchemas and configDefaults
for (const id of Object.keys(this.plugins)) {
const { config, key } = this.plugins[id];
if (config.schema) this.configSchemas[key] = config.schema;
if (config.default !== undefined) {
this.configDefaults[key] = config.default;
}
this.configOnChanges[key] = config.onChange || ConfigChangeType.reload;
}
可以看到,这些信息最后被放在 service的三个对象里面,分别是 this.configSchemas[key], this.configDefaults[key], this.configOnChanges[key],后面会用到。
到这里初始化阶段已经基本完成,下一篇我们看 resolve 阶段。