umi阅读分为5个部分,分别是:
以下代码以umi的3.5.20版本为例,主要内容以源码+个人解读为主
插件机制
umi的另一个特点是一切皆插件,本部分内容主要研究插件机制,包括注册,初始化。
主要内容:研究插件如何注册和执行的过程,并了解插件实现的机制。
v3.umijs.org/zh-CN/guide… umi官网中关于插件开发的内容中简单描述了如何开发一个插件,下面是官网的简单示例:
export default function (api: IApi) {
api.logger.info('use plugin');
api.describe({
key: 'mainPath',
config: {
schema(joi) {
return joi.string();
},
},
});
api.modifyHTML(($) => {
$('body').prepend(`<h1>hello umi plugin</h1>`);
return $;
});
if (api.userConfig.mainPath) {
api.modifyRoutes((routes: any[]) => {
return resetMainPath(routes, api.config.mainPath);
});
}
}
显然这里的api对象是从外部注入的,那api对象是什么呢?
带着这个问题我们来看代码。首先是插件引用:命名规则使用 @umijs 或者 umi-plugin 开头;或在config中显示引用。接下来我们看umi是怎么处理这两种插件规则的。
也是dev执行过程去查找。
在packages/core/src/Service/Service.ts中Service类中,实例化函数中
// 初始化插件集
this.initialPresets = resolvePresets({
...baseOpts,
presets: opts.presets || [],
userConfigPresets: this.userConfig.presets || [],
});
// 初始化插件
this.initialPlugins = resolvePlugins({
...baseOpts,
plugins: opts.plugins || [],
userConfigPlugins: this.userConfig.plugins || [],
});
继续看调用的函数
// 处理插件集
export function resolvePresets(opts: IResolvePresetsOpts) {
const type = PluginType.preset;
const presets = [...getPluginsOrPresets(type, opts)];
return presets.map((path: string) => {
return pathToObj({
type,
path,
cwd: opts.cwd,
});
});
}
// 处理插件
export function resolvePlugins(opts: IResolvePluginsOpts) {
const type = PluginType.plugin;
const plugins = getPluginsOrPresets(type, opts);
return plugins.map((path: string) => {
return pathToObj({
type,
path,
cwd: opts.cwd,
});
});
}
在resolvePresets和resolvePlugins中,都调用了getPluginsOrPresets:
function getPluginsOrPresets(type: PluginType, opts: IOpts): string[] {
const upperCaseType = type.toUpperCase();
return [
// opts
...((opts[type === PluginType.preset ? 'presets' : 'plugins'] as any) ||
[]),
// env
...(process.env[`UMI_${upperCaseType}S`] || '').split(',').filter(Boolean),
// dependencies
...Object.keys(opts.pkg.dependencies || {})
.concat(Object.keys(opts.pkg.dependencies || {}))
.filter(isPluginOrPreset.bind(null, type)),
// user config
...((opts[
type === PluginType.preset ? 'userConfigPresets' : 'userConfigPlugins'
] as any) || []),
].map((path) => {
return resolve.sync(path, {
basedir: opts.cwd,
extensions: ['.js', '.ts'],
});
});
}
getPluginsOrPresets中获取来着以下几个途径的插件:
(1)内置的,即opts
(2)环境变量中[UMI_${upperCaseType}S]注入的
(3)packages.json文件dependencies和dependencies中符合插件命名规则的插件
(4)用户配置中的显示引用插件
其中(3)(4)便是我们常用的外部插件引用的方式。这里的引用只是返回模块的路径。所有的路径还会进行依次转换,调用的是pathToObj函数
export function pathToObj({
type,
path,
cwd,
}: {
type: PluginType;
path: string;
cwd: string;
}) {
let pkg = null;
let isPkgPlugin = false;
const pkgJSONPath = pkgUp.sync({ cwd: path });
if (pkgJSONPath) {
pkg = require(pkgJSONPath);
isPkgPlugin =
winPath(join(dirname(pkgJSONPath), pkg.main || 'index.js')) ===
winPath(path);
}
// 生成规范id
let id;
if (isPkgPlugin) {
id = pkg!.name;
} else if (winPath(path).startsWith(winPath(cwd))) {
id = `./${winPath(relative(cwd, path))}`;
} else if (pkgJSONPath) {
id = winPath(join(pkg!.name, relative(dirname(pkgJSONPath), path)));
} else {
id = winPath(path);
}
id = id.replace('@umijs/preset-built-in/lib/plugins', '@@');
id = id.replace(/.js$/, '');
// 生成规范key
const key = isPkgPlugin
? pkgNameToKey(pkg!.name, type)
: nameToKey(basename(path, extname(path)));
// 返回规范化对象
return {
id,
key,
path: winPath(path),
apply() {
// 使用插件的时候则是调用apply函数获取,延迟导入函数对象
try {
const ret = require(path);
// 导出es模块
return compatESModuleRequire(ret);
} catch (e) {
throw new Error(`Register ${type} ${path} failed, since ${e.message}`);
}
},
defaultConfig: null,
};
}
获得规范化的插件对象之后,我们继续研究怎么注册到umi的Service对象中,前面说到service实例化之后,运行了service.run()的方法,我们也确实在该方法中找到了初始化插件的函数
在service中init函数调用了initPresetsAndPlugins函数初始化插件:
async initPresetsAndPlugins() {
// 遍历初始化preset
this.setStage(ServiceStage.initPresets);
this._extraPlugins = [];
while (this.initialPresets.length) {
await this.initPreset(this.initialPresets.shift()!);
}
// 遍历初始化plugin
this.setStage(ServiceStage.initPlugins);
this._extraPlugins.push(...this.initialPlugins);
while (this._extraPlugins.length) {
await this.initPlugin(this._extraPlugins.shift()!);
}
}
遍历调用initPreset来初始化preset,遍历调用initPlugin来初始化plugin,先看initPlugin
async initPlugin(plugin: IPlugin) {
const { id, key, apply } = plugin;
// 获取独立的api实例
const api = this.getPluginAPI({ id, key, service: this });
// 注册插件
this.registerPlugin(plugin);
// api实例注入
await this.applyAPI({ api, apply });
}
initPlugin执行了3个操作,依次来看。获取api实例,注册插件,和api注入。
理解这里的操作的设计,先区分两个概念,plugin和pluginAPI,前者是插件,而pluginAPI是注册管理插件的类,用来代理注册插件到service中,提供插件基本使用的api。简单来说,pluginAPI承担plugin和service的中间角色。
getPluginAPI
getPluginAPI(opts: any) {
const pluginAPI = new PluginAPI(opts);
// register built-in methods
[
'onPluginReady',
'modifyPaths',
'onStart',
'modifyDefaultConfig',
'modifyConfig',
].forEach((name) => {
pluginAPI.registerMethod({ name, exitsError: false });
});
return new Proxy(pluginAPI, {
get: (target, prop: string) => {
// 由于 pluginMethods 需要在 register 阶段可用
// 必须通过 proxy 的方式动态获取最新,以实现边注册边使用的效果
if (this.pluginMethods[prop]) return this.pluginMethods[prop];
if (
[
'applyPlugins',
'ApplyPluginsType',
'EnableBy',
'ConfigChangeType',
'babelRegister',
'stage',
'ServiceStage',
'paths',
'cwd',
'pkg',
'userConfig',
'config',
'env',
'args',
'hasPlugins',
'hasPresets',
].includes(prop)
) {
return typeof this[prop] === 'function'
? this[prop].bind(this)
: this[prop];
}
return target[prop];
},
});
}
pluginAPI.registerMethod的实际操作,name存在则不会重复注册
pluginAPI.registerMethod({name,fn}) === this.service.pluginMethods[name] = fn
getPluginAPI返回了一个代理对象Proxy,从代码逻辑来看,是通过pluginAPI代理了servece部分操作,api.prop实际调用的是service.prop。
registerPlugin
registerPlugin(plugin: IPlugin) {
if (this.plugins[plugin.id]) {
// 校验
}
// 根据id挂接插件对象
this.plugins[plugin.id] = plugin;
}
applyAPI
async applyAPI(opts: { apply: Function; api: PluginAPI }) {
// 调用插件apply,并注入api
let ret = opts.apply()(opts.api);
if (isPromise(ret)) {
ret = await ret;
}
return ret || {};
}
apply上面说明了是延迟导入插件对象的操作,apply()返回的标识插件对象,然后再注入api,api是PluginAPI的实例化对象,该对象用于管理注册plugin,并提供一些service的api能力,包括modifyHTML之类的。
再看一下initPreset,对preset的不同特性进行了处理。
async initPreset(preset: IPreset) {
const { id, key, apply } = preset;
preset.isPreset = true;
const api = this.getPluginAPI({ id, key, service: this });
// register before apply
this.registerPlugin(preset);
const { presets, plugins, ...defaultConfigs } = await this.applyAPI({
api,
apply,
});
// 提供了preset和plugin可以注册preset和plugin的操作
if (presets) {
// 插到最前面,下个 while 循环优先执行
this._extraPresets.splice(
0,
0,
...presets.map((path: string) => {
return pathToObj({
type: PluginType.preset,
path,
cwd: this.cwd,
});
}),
);
}
// 深度优先
const extraPresets = lodash.clone(this._extraPresets);
this._extraPresets = [];
while (extraPresets.length) {
await this.initPreset(extraPresets.shift()!);
}
// plugin交给下一步初始化plugin来操作
if (plugins) {
this._extraPlugins.push(
...plugins.map((path: string) => {
return pathToObj({
type: PluginType.plugin,
path,
cwd: this.cwd,
});
}),
);
}
}
插件的调用applyPlugins:
async applyPlugins(opts: {
key: string;
type: ApplyPluginsType;
initialValue?: any;
args?: any;
}) {
const hooks = this.hooks[opts.key] || [];
switch (opts.type) {
case ApplyPluginsType.add:
const tAdd = new AsyncSeriesWaterfallHook(['memo']);
for (const hook of hooks) {
if (!this.isPluginEnable(hook.pluginId!)) {
continue;
}
tAdd.tapPromise(
{
name: hook.pluginId!,
stage: hook.stage || 0,
// @ts-ignore
before: hook.before,
},
async (memo: any[]) => {
const items = await hook.fn(opts.args);
return memo.concat(items);
},
);
}
return await tAdd.promise(opts.initialValue || []);
case ApplyPluginsType.modify:
const tModify = new AsyncSeriesWaterfallHook(['memo']);
for (const hook of hooks) {
if (!this.isPluginEnable(hook.pluginId!)) {
continue;
}
tModify.tapPromise(
{
name: hook.pluginId!,
stage: hook.stage || 0,
// @ts-ignore
before: hook.before,
},
async (memo: any) => {
return await hook.fn(memo, opts.args);
},
);
}
return await tModify.promise(opts.initialValue);
case ApplyPluginsType.event:
const tEvent = new AsyncSeriesWaterfallHook(['_']);
for (const hook of hooks) {
if (!this.isPluginEnable(hook.pluginId!)) {
continue;
}
tEvent.tapPromise(
{
name: hook.pluginId!,
stage: hook.stage || 0,
// @ts-ignore
before: hook.before,
},
async () => {
await hook.fn(opts.args);
},
);
}
return await tEvent.promise();
default:
throw new Error(
`applyPlugin failed, type is not defined or is not matched, got ${opts.type}.`,
);
}
AsyncSeriesWaterfallHook时一个异步顺序执行的函数,上一个异步函数的返回值会传给下一个函数,因此可以执行一些同名钩子函数。
PluginAPI的其他api在官网中都有描述v3.umijs.org/zh-CN/plugi…,包括描述,调用等。实现也不复杂,这里不再赘述。
以上就是钩子机制的研究解读。
说在后面:
umi的插件时较为复杂和庞大,理清逻辑需要耗费很大的功夫,也借鉴了不少大佬的解读文档和思路。umi提供了很多的预置插件来完善插件的功能,如果去除大量的插件外,umi的核心框架代码还是比较少的。
umi作为可插拔的企业级框架,通过约定式,插件机制等设计理念,提供了一个大而全且可拓展的前端开发框架。
有些人会觉得umi过于臃肿,当然对于小型项目来说,umi很多非必要的东西确实会影响性能。但是笔者觉得作为大而全的框架,必然会牺牲掉一些东西,umi的后续版本也在优化开发体验上不断努力。
当然如果我们不需要参与维护umi代码,可以不用把umi的实现过程搞得非常清楚,通过研究umi的主要源码,笔者总结框架可学习的内容。
设计理念:
(1)约定大于配置:比如按照他说明的格式创建文件即可开通和使用功能,比如创建
mock文件夹则开启mock数据功能。 好处有:项目代码结构清晰,需要什么知道找到文件夹/文件即可;减少配置的负担。(2)一切皆插件:umi通过将各种操作进行插件抽象化,在此基础上不但实现了框架原有的功能,还提供了外部可插拔的插件机制。这种设计我们可以应用在一些组件框架的设计之中。当然这种插件化的设计会在一定程度上破坏流程的连续性,但是提高了灵活性。
(完)
参考资料: