核心概念
preset 和plugin
preset 的作用是预设一些插件,它通常用来注册一批 presets 和 plugins。在 preset 中,上述提到的接受 api 的方法可以有返回值,该返回值是一个包含 plugins 和 presets 属性的对象,其作用就是注册相应的插件或者插件集。
import { IApi } from 'umi';
export default (api: IApi) => {
return {
plugins: ['./plugin_foo','./plugin_bar'],
presets: [
// npm 依赖
'umi-preset-hello',
// 相对路径
'./preset',
// 绝对路径
`${__dirname}/preset.js`,
],
}
};
它们的注册顺序是值得注意的:presets 始终先于 plugins 注册。Umi 维护了两个队列分别用来依次注册 presets 和 plugins,这个例子中的注册的
preset_foo将被置于 presets 队列队首,而plugin_foo和plugin_bar将被依次置于 plugins 队列队尾。这里把 preset 放在队首的目的在于保证 presets 之间的顺序和关系是可控的。
packages/core/src/service/service.ts
生命周期
`` export enum ServiceStage { uninitialized, init, initPresets, initPlugins, resolveConfig, collectAppData, onCheck, onStart, runCommand, }
### init阶段
#### init stage
>- init stage: 该阶段 Umi 将加载各类配置信息。包括:加载 `.env` 文件; require `package.json` ;加载用户的配置信息; resolve 所有的插件(内置插件、环境变量、用户配置依次进行)
``` js
// packages/core/src/service/service.ts run()
async run(opts: { name: string; args?: any }) {
const { name, args = {} } = opts;
args._ = args._ || [];
// shift the command itself
if (args._[0] === name) args._.shift();
this.args = args;
this.name = name;
// loadEnv
this.stage = ServiceStage.init;
loadEnv({ cwd: this.cwd, envFile: '.env' });
// get pkg from package.json
let pkg: Record<string, string | Record<string, any>> = {};
let pkgPath: string = '';
try {
pkg = require(join(this.cwd, 'package.json'));
pkgPath = join(this.cwd, 'package.json');
} catch (_e) {
// APP_ROOT
if (this.cwd !== process.cwd()) {
try {
pkg = require(join(process.cwd(), 'package.json'));
pkgPath = join(process.cwd(), 'package.json');
} catch (_e) {}
}
}
this.pkg = pkg;
this.pkgPath = pkgPath || join(this.cwd, 'package.json');
const prefix = this.frameworkName;
const specifiedEnv = process.env[`${prefix}_ENV`.toUpperCase()];
// https://github.com/umijs/umi/pull/9105
// assert(
// !specifiedEnv ||
// (specifiedEnv && !Object.values(SHORT_ENV).includes(specifiedEnv)),
// `${chalk.yellow(
// Object.values(SHORT_ENV).join(', '),
// )} config files will be auto loaded by env, Do not configure ${chalk.cyan(
// `process.env.${prefix.toUpperCase()}_ENV`,
// )} with these values`,
// );
// get user config
const configManager = new Config({
cwd: this.cwd,
env: this.env,
defaultConfigFiles: this.opts.defaultConfigFiles,
specifiedEnv,
});
this.configManager = configManager;
this.userConfig = configManager.getUserConfig().config;
// get paths
// temporary paths for use by function generateFinalConfig.
// the value of paths may be updated by plugins later
// 抽离成函数,方便后续继承覆盖
this.paths = await this.getPaths();
}
注册阶段
initPresets stage
- initPresets stage: 该阶段 Umi 将注册 presets。presets 在注册的时候可以通过
return { presets, plugins }来添加额外的插件。其中 presets 将添加到 presets 队列的队首,而 plugins 将被添加到 plugins 队列的队尾。
initPlugins stage
- initPlugins stage: 该阶段 Umi 将注册 plugins。这里的 plugins 包括上个阶段由 presets 添加的额外的 plugins, 一个值得注意的点在于: 尽管 plugins 也可以
return { presets, plugins },但是 Umi 不会对其进行任何操作。插件的 init 其实就是执行插件的代码(但是插件的代码本质其实只是调用 api 进行各种 hook 的注册,而 hook 的执行并非在此阶段执行,因此这里叫插件的注册)。
// packages/core/src/service/service.ts run()
// resolve initial presets and plugins
const { plugins, presets } = Plugin.getPluginsAndPresets({
cwd: this.cwd,
pkg,
plugins: [require.resolve('./generatePlugin')].concat(
this.opts.plugins || [],
),
presets: [require.resolve('./servicePlugin')].concat(
this.opts.presets || [],
),
userConfig: this.userConfig,
prefix,
});
// register presets and plugins
this.stage = ServiceStage.initPresets;
const presetPlugins: Plugin[] = [];
while (presets.length) {
await this.initPreset({
preset: presets.shift()!,
presets,
plugins: presetPlugins,
});
}
plugins.unshift(...presetPlugins);
this.stage = ServiceStage.initPlugins;
while (plugins.length) {
await this.initPlugin({ plugin: plugins.shift()!, plugins });
}
const command = this.commands[name];
if (!command) {
this.commandGuessHelper(Object.keys(this.commands), name);
throw Error(`Invalid command ${chalk.red(name)}, it's not registered.`);
}
// 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;
}
initPreset
async initPreset(opts: {
preset: Plugin;
presets: Plugin[];
plugins: Plugin[];
}) {
const { presets, plugins } = await this.initPlugin({
plugin: opts.preset,
presets: opts.presets,
plugins: opts.plugins,
});
// presets 将添加到 presets 队列的队首
opts.presets.unshift(...(presets || []));
// plugins 将被添加到 plugins 队列的队尾
opts.plugins.push(...(plugins || []));
}
initPlugins
通过pluginAPI.registerPresets和pluginAPI.registerPlugins 进行注册
async initPlugin(opts: {
plugin: Plugin;
presets?: Plugin[];
plugins: Plugin[];
}) {
// register to this.plugins
assert(
!this.plugins[opts.plugin.id],
`${opts.plugin.type} ${opts.plugin.id} is already registered by ${
this.plugins[opts.plugin.id]?.path
}, ${opts.plugin.type} from ${opts.plugin.path} register failed.`,
);
this.plugins[opts.plugin.id] = opts.plugin;
// apply with PluginAPI
const pluginAPI = new PluginAPI({
plugin: opts.plugin,
service: this,
});
pluginAPI.registerPresets = pluginAPI.registerPresets.bind(
pluginAPI,
opts.presets || [],
);
pluginAPI.registerPlugins = pluginAPI.registerPlugins.bind(
pluginAPI,
opts.plugins,
);
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,
},
});
let dateStart = new Date();
let ret = await opts.plugin.apply()(proxyPluginAPI);
opts.plugin.time.register = new Date().getTime() - dateStart.getTime();
if (opts.plugin.type === 'plugin') {
assert(!ret, `plugin should return nothing`);
}
// key should be unique
assert(
!this.keyToPluginMap[opts.plugin.key],
`key ${opts.plugin.key} is already registered by ${
this.keyToPluginMap[opts.plugin.key]?.path
}, ${opts.plugin.type} from ${opts.plugin.path} register failed.`,
);
this.keyToPluginMap[opts.plugin.key] = opts.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,
}),
);
}
return ret || {};
}
packages/core/src/service/pluginAPI.ts
registerPresets
export class PluginAPI {
registerPresets(source: Plugin[], presets: any[]) {
assert(
this.service.stage === ServiceStage.initPresets,
`api.registerPresets() failed, it should only used in presets.`,
);
source.splice(
0,
0,
...presets.map((preset) => {
return new Plugin({
path: preset,
cwd: this.service.cwd,
type: PluginType.preset,
});
}),
);
}
}
registerPlugins
export class PluginAPI {
registerPlugins(source: Plugin[], plugins: any[]) {
assert(
this.service.stage === ServiceStage.initPresets ||
this.service.stage === ServiceStage.initPlugins,
`api.registerPlugins() failed, it should only be used in registering stage.`,
);
const mappedPlugins = plugins.map((plugin) => {
if (lodash.isPlainObject(plugin)) {
assert(
plugin.id && plugin.key,
`Invalid plugin object, id and key must supplied.`,
);
plugin.type = PluginType.plugin;
plugin.enableBy = plugin.enableBy || EnableBy.register;
plugin.apply = plugin.apply || (() => () => {});
plugin.config = plugin.config || {};
plugin.time = { hooks: {} };
return plugin;
} else {
return new Plugin({
path: plugin,
cwd: this.service.cwd,
type: PluginType.plugin,
});
}
});
if (this.service.stage === ServiceStage.initPresets) {
source.push(...mappedPlugins);
} else {
source.splice(0, 0, ...mappedPlugins);
}
}
}
执行阶段
主要利用applyPlugins来完成
resolveConfig stage
- resolveConfig stage: 该阶段 Umi 将整理各个插件中对于
config schema的定义,然后执行插件的modifyConfig、modifyDefaultConfig、modifyPaths等 hook,进行配置的收集。
// setup api.config from modifyConfig and modifyDefaultConfig
this.stage = ServiceStage.resolveConfig;
const { defaultConfig } = await this.resolveConfig();
if (this.config.outputPath) {
this.paths.absOutputPath = isAbsolute(this.config.outputPath)
? this.config.outputPath
: join(this.cwd, this.config.outputPath);
}
this.paths = await this.applyPlugins({
key: 'modifyPaths',
initialValue: this.paths,
});
const storage = await this.applyPlugins({
key: 'modifyTelemetryStorage',
initialValue: noopStorage,
});
this.telemetry.useStorage(storage);
async resolveConfig() {
// configManager and paths are not available until the init stage
assert(
this.stage > ServiceStage.init,
`Can't generate final config before init stage`,
);
const resolveMode = this.commands[this.name].configResolveMode;
const config = await this.applyPlugins({
key: 'modifyConfig',
// why clone deep?
// user may change the config in modifyConfig
// e.g. memo.alias = xxx
initialValue: lodash.cloneDeep(
resolveMode === 'strict'
? this.configManager!.getConfig({
schemas: this.configSchemas,
}).config
: this.configManager!.getUserConfig().config,
),
args: { paths: this.paths },
});
const defaultConfig = await this.applyPlugins({
key: 'modifyDefaultConfig',
// 避免 modifyDefaultConfig 时修改 this.configDefaults
initialValue: lodash.cloneDeep(this.configDefaults),
});
this.config = lodash.merge(defaultConfig, config) as Record<string, any>;
return { config, defaultConfig };
}
collectionAppData stage
- collectionAppData stage: 该阶段 Umi 执行
modifyAppDatahook,来维护 App 的元数据。(AppData是umi@4新增的 api )
// applyPlugin collect app data
// TODO: some data is mutable
this.stage = ServiceStage.collectAppData;
this.appData = await this.applyPlugins({
key: 'modifyAppData',
initialValue: {
// base
cwd: this.cwd,
pkg,
pkgPath,
plugins: this.plugins,
presets,
name,
args,
// config
userConfig: this.userConfig,
mainConfigFile: configManager.mainConfigFile,
config: this.config,
defaultConfig: defaultConfig,
// TODO
// moduleGraph,
// routes,
// npmClient,
// nodeVersion,
// gitInfo,
// gitBranch,
// debugger info,
// devPort,
// devHost,
// env
},
});
```
#### onCheck stage
> - onCheck stage: 该阶段 Umi 执行 `onCheck` hook。
// applyPlugin onCheck
this.stage = ServiceStage.onCheck;
await this.applyPlugins({
key: 'onCheck',
});
```
#### onStart stage
- onStart stage: 该阶段 Umi 执行
onStarthook。
// applyPlugin onStart
this.stage = ServiceStage.onStart;
await this.applyPlugins({
key: 'onStart',
});
run command 阶段
- runCommand stage: 该阶段 Umi 运行当前 cli 要执行的 command,(例如
umi dev, 这里就会执行 dev command)Umi 的各种核心功能都在 command 中实现。包括我们的插件调用 api 注册的绝大多数 hook。
// run command
this.stage = ServiceStage.runCommand;
let ret = await command.fn({ args });
this._profilePlugins();
以上就是 Umi 的插件机制的整体流程。
core API
packages/core/src/service/pluginAPI.ts
register
api.register({ key: string, fn, before?: string, stage?: number})
为 api.applyPlugins 注册可供其使用的 hook。
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 }),
);
}
值得注意的是, fn的写法要结合即将使用的applyPlugin的type参数来确定
type
export enum ApplyPluginsType {
add = 'add',
modify = 'modify',
event = 'event',
}
type等于add, 当key以add开头且没有显示声明type时, applyPlugins 会默认按此类型执行, fn 需有返回值, applyPlugins 的 initialValue 必须是一个数组
await api.register({
key: 'addFoo',
fn: async (args) => args * 2
})
api.applyPlugins({
key: 'addFoo',
initialValue: [],
args: 1
}).then(data=>{
// [2]
})
type等于modify, 当key以modify开头且没有显示声明type时, applyPlugins 会默认按此类型执行, 需要initialValue,fn 需要返回修改后的memo
await api.register({
key: 'modifyMasterHTML',
fn: (memo, args) => ({ ...memo, a: args})
})
api.applyPlugins({
key: 'modifyMasterHTML',
type: api.ApplyPluginsType.modify, // 可以不写
initialValue: $.html(),
});
type等于event,当key以on开头且没有显示声明type时, applyPlugins 会默认按此类型执行,不需要fn 和initvalues
await api.register({
key: 'onCheckCode'
})
api.applyPlugins({
key: 'onCheckCode',
args: {
...args,
CodeFrameError,
},
sync: true,
});
applyPlugins
api.applyPlugins({ key: string, type?: api.ApplyPluginsType, initialValue?: any, args?: any })
取得 register() 注册的 hooks 执行后的数据
核心本质: 插件的代码本质其实只是调用 api 进行各种 hook 的注册,而 hook 的执行并非在此阶段执行、执行 功能: 修改代码打包配置、修改启动代码、约定目录结构、修改 HTML等等
packages/core/src/service/pluginAPI.ts
registerMethod
api.registerMethod({ name: string, fn? })
往 api 上注册一个名为 'name' 的方法。
- 当传入了 fn 时,执行 fn
- 当没有传入 fn 时,
registerMethod会将name作为api.register的key并且将其柯里化后作为fn。这种情况下相当于注册了一个register的快捷调用方式,便于注册 hook。
// 这里的函数柯里话化表现在哪里?
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 }),
});
},
};
}
preset-umi
@umijs/core 提供了一套插件的注册和管理机制, 核心的功能都依靠preset-umi来实现
packages/preset-umi/src/index.ts
- registerMethods 这类插件注册了一些上述提到的“注册器”,以供开发者快速地注册 hook,这类方法也占据了 PluginAPI 中的大多数。
// packages/preset-umi/src/registerMethods.ts
export default (api: IApi) => {
[
'onGenerateFiles',
'onBeforeCompiler',
'onBuildComplete',
'onBuildHtmlComplete',
'onPatchRoute',
'onPkgJSONChanged',
'onPrepareBuildSuccess',
'onDevCompileDone',
'onCheckPkgJSON',
'onCheckCode',
'onCheckConfig',
'onBeforeMiddleware',
'addBeforeMiddlewares',
'addLayouts',
'addMiddlewares',
'addApiMiddlewares',
'addRuntimePlugin',
'addRuntimePluginKey',
'addPolyfillImports',
'addPrepareBuildPlugins',
'addEntryImportsAhead',
'addEntryImports',
'addEntryCodeAhead',
'addEntryCode',
'addExtraBabelPresets',
'addExtraBabelPlugins',
'addBeforeBabelPresets',
'addBeforeBabelPlugins',
'addHTMLMetas',
'addHTMLLinks',
'addHTMLStyles',
'addHTMLHeadScripts',
...
].forEach((name) => {
api.registerMethod({ name });
});
- features 这类插件为 Umi 提供了一些特性,例如 appData、lowImport、mock 等。
packages/preset-umi/src/features
- commands 这类插件注册了各类 command, 提供了 Umi CLI 的各种功能。Umi 能够在终端中正常运行,依靠的就是 command 提供的功能。
packages/preset-umi/src/commands