第4篇 《04.run 方法-收集插件和插件集》 中我们介绍了umi内置的插件集和插件,这里再汇总一下,包括三个方面,这三个是有顺序的,因为umi中插件集合优先于插件注册。
- 插件 servicePlugin 注册方法(最终被放在presets里面了,因此先执行)
- 插件集 @umijs/preset-umi 多种插件的集合
- 插件 generatePlugin 注册命令
下面会针对这些插件展开说明
servicePlugin
packages/core/src/service/servicePlugin.ts 代码和简单,初始化以下方法
export default (api: PluginAPI) => {
[
'onCheck',
'onStart',
'modifyAppData',
'modifyConfig',
'modifyDefaultConfig',
'modifyPaths',
].forEach((name) => {
api.registerMethod({ name });
});
};
这里再复习一下 registerMethod
其实核心逻辑就是在 this.service.pluginMethods 注册一个方法
类似这样 this.service.pluginMethods.modifyPaths = {fn: function(){}}
然后可以通过代理 proxyPluginAPI 的 opts.service.pluginMethods 调用 fn,即 api.onCheck(() => {} )
if (opts.service.pluginMethods[prop]) {
return opts.service.pluginMethods[prop].fn;
}
下面是 registerMethod 的核心代码
registerMethod(opts: { name: string; fn?: Function }) {
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 }),
});
},
};
}
下面是代理调用的核心代码,调用api.onCheck 就是调用 opts.service.pluginMethods['onCheck'] = fn
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;
}
// @ts-ignore
return target[prop];
},
});
}
@umijs/preset-umi
插件集中有几十个插件,我们这里挑其中几个来说明
registerMethods
主要分为三个部分
- 注册大量的方法
- 执行一个 onStart
- 注册一个
writeTmpFile方法,后面可以调用,源码中都是通过api.writeTmpFile的方式调用的。
注册了 writeTmpFile之后怎么就能通过api.writeTmpFile 访问到对应注册的方法
实现原理上面已经复习过,此处不再赘述
writeTmpFile 简单来说就是把传进来的内容做一些处理,然后生成一个文件 writeFileSync(absPath, content, 'utf-8');当我们调用api.writeTmpFile()时,这个方法就会执行。
import { init } from '@umijs/bundler-utils/compiled/es-module-lexer';
import { fsExtra, lodash, Mustache } from '@umijs/utils';
import assert from 'assert';
import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import { IApi } from './types';
import { isTypeScriptFile } from './utils/isTypeScriptFile';
import transformIEAR from './utils/transformIEAR';
export default (api: IApi) => {
[
'onGenerateFiles',
'onBeforeCompiler',
'onBuildComplete',
'onBuildHtmlComplete',
'onPatchRoute',
// 'onPatchRouteBefore',
// 'onPatchRoutes',
// 'onPatchRoutesBefore',
'onPkgJSONChanged', // new
'onDevCompileDone',
'onCheckPkgJSON',
'onCheckCode',
'onCheckConfig',
'onBeforeMiddleware',
'addBeforeMiddlewares',
'addLayouts',
'addMiddlewares',
'addApiMiddlewares',
'addRuntimePlugin',
'addRuntimePluginKey',
// 'addUmiExports',
'addPolyfillImports',
'addEntryImportsAhead',
'addEntryImports',
'addEntryCodeAhead',
'addEntryCode',
'addExtraBabelPresets',
'addExtraBabelPlugins',
'addBeforeBabelPresets',
'addBeforeBabelPlugins',
'addHTMLMetas',
'addHTMLLinks',
'addHTMLStyles',
'addHTMLHeadScripts',
'addHTMLScripts',
'addTmpGenerateWatcherPaths',
'chainWebpack',
'modifyEntry',
'modifyHTMLFavicon',
'modifyHTML',
'modifyExportHTMLFiles',
'modifyWebpackConfig',
'modifyViteConfig',
// 'modifyHTMLChunks',
// 'modifyExportRouteMap',
// 'modifyPublicPathStr',
'modifyRendererPath',
'modifyServerRendererPath',
'modifyRoutes',
'modifyBabelPresetOpts',
].forEach((name) => {
api.registerMethod({ name });
});
api.onStart(async () => {
await init;
});
api.registerMethod({
name: 'writeTmpFile',
fn(opts: {
path: string;
content?: string;
tpl?: string;
tplPath?: string;
context?: Record<string, string>;
noPluginDir?: boolean;
}) {
const absPath = join(
api.paths.absTmpPath,
// @ts-ignore
this.plugin.key && !opts.noPluginDir ? `plugin-${this.plugin.key}` : '',
opts.path,
);
fsExtra.mkdirpSync(dirname(absPath));
let content = opts.content;
if (!content) {
const tpl = opts.tplPath
? readFileSync(opts.tplPath, 'utf-8')
: opts.tpl;
content = Mustache.render(tpl, opts.context);
}
// Only js files generate comments
const isJsFile = /\.(t|j)sx?$/.test(absPath);
content = [
isTypeScriptFile(opts.path) && `// @ts-nocheck`,
isJsFile && '// This file is generated by Umi automatically',
isJsFile && '// DO NOT CHANGE IT MANUALLY!',
content.trim(),
'',
]
.filter((text) => text !== false)
.join('\n');
// transform imports for all javascript-like files only vite mode enable
if (api.appData.vite && isJsFile) {
content = transformIEAR({ content, path: absPath }, api);
}
if (!existsSync(absPath)) {
writeFileSync(absPath, content, 'utf-8');
} else {
const fileContent = readFileSync(absPath, 'utf-8');
if (fileContent.startsWith('// debug') || fileContent === content) {
return;
} else {
writeFileSync(absPath, content, 'utf-8');
}
}
},
});
};
commands/dev/dev
我们再来看一下dev ,看看一个命令是怎么把项目跑起来的。 执行命令前我们自然是要先注册这个命令,注册过程也是通过插件注册的。 这里看一下核心代码, 去除函数体 fn 的内容,具体看命令是怎么注册进来的。
export default (api: IApi) => {
api.describe({
enableBy() {
return api.name === 'dev';
},
});
api.registerCommand({
name: 'dev',
description: 'dev server for development',
details: `
umi dev
# dev with specified port
PORT=8888 umi dev
`,
async fn() {},
});
};
稍微详细点的代码如下
registerCommand(
opts: Omit<ICommandOpts, 'plugin'> & { alias?: string | string[] },
) {
const { alias } = opts;
delete opts.alias;
const registerCommand = (commandOpts: Omit<typeof opts, 'alias'>) => {
const { name } = commandOpts;
this.service.commands[name] = new Command({
...commandOpts,
plugin: this.plugin,
});
};
registerCommand(opts);
if (alias) {
const aliases = makeArray(alias);
aliases.forEach((alias) => {
registerCommand({ ...opts, name: alias });
});
}
}
export class Command {
name: string;
description?: string;
options?: string;
details?: string;
configResolveMode: ResolveConfigMode;
fn: {
({ args }: { args: yParser.Arguments }): void;
};
plugin: Plugin;
constructor(opts: IOpts) {
this.name = opts.name;
this.description = opts.description;
this.options = opts.options;
this.details = opts.details;
this.fn = opts.fn;
this.plugin = opts.plugin;
this.configResolveMode = opts.configResolveMode || 'strict';
}
}
其实核心逻辑很简单,只是在this.service.commands 对象下新增了一个命令,然后实例化new Command
this.service.commands[name] = new Command({
...commandOpts,
plugin: this.plugin,
});
};
当我们执行 umi dev 时,umi的 umi/packages/core/src/service/service.ts 会做两步
- 第一步从 commands 中找到命令
const command = this.commands[name]; - 第二步 执行
registerCommand注册的命令对应的fn函数体await command.fn({ args });
通过这两步骤我们就把整个项目关联起来了
generatePlugin
注册一个 generate 命令,用于生成命令的命令,可以以插件的形式调用。 generate命令的注册方法跟上面 注册 dev 命令类似,这里不再详细介绍,主要说一下 generate对应的 fn 做了什么事。
async fn({ args }) {
const [type] = args._;
const runGenerator = async (generator: Generator) => {
await generator?.fn({
args,
generateFile,
installDeps,
updatePackageJSON,
});
};
if (type) {
const generator = api.service.generators[type];
if (!generator) {
throw new Error(`Generator ${type} not found.`);
}
if (generator.type === GeneratorType.enable) {
const enable = await generator.checkEnable?.({
args,
});
if (!enable) {
if (typeof generator.disabledDescription === 'function') {
logger.warn(generator.disabledDescription());
} else {
logger.warn(generator.disabledDescription);
}
return;
}
}
await runGenerator(generator);
} else {
const getEnableGenerators = async (
generators: typeof api.service.generators,
) => {
const questions = [] as { title: string; value: string }[];
for (const key of Object.keys(generators)) {
const g = generators[key];
if (g.type === GeneratorType.generate) {
questions.push({
title: `${g.name} -- ${g.description}` || '',
value: g.key,
});
} else {
const enable = await g?.checkEnable?.({
args,
});
if (enable) {
questions.push({
title: `${g.name} -- ${g.description}` || '',
value: g.key,
});
}
}
}
return questions;
};
const questions = await getEnableGenerators(api.service.generators);
const { gType } = await prompts({
type: 'select',
name: 'gType',
message: 'Pick generator type',
choices: questions,
});
await runGenerator(api.service.generators[gType]);
}
},
其实核心核心代码就三部分
- 第一部分根据不同的type生成
questions - 第二部分通过 prompts 注册 node 的 命令行工具,使用户具备和 node 命令窗口交互的能力。具体看文档 prompts 链接
- 第三部分是通过找到
api.service.generators对象中的对应type,然后执行这个type下的fn
const questions = await getEnableGenerators(api.service.generators);
const { gType } = await prompts({
type: 'select',
name: 'gType',
message: 'Pick generator type',
choices: questions,
});
await runGenerator(api.service.generators[gType]);
const runGenerator = async (generator: Generator) => {
await generator?.fn({
args,
generateFile,
installDeps,
updatePackageJSON,
});
};
要说 api.service.generators,就不得不说 registerGenerator下面我们就具体看下
registerGenerator 是怎么为 api.service.generators 添加不同的type和fn的。
registerGenerator
我们来看 /packages/core/src/service/pluginAPI.ts,找到 registerGenerator
registerGenerator(opts: DistributiveOmit<Generator, 'plugin'>) {
const { key } = opts;
this.service.generators[key] = makeGenerator({
...opts,
plugin: this.plugin,
});
}
export function makeGenerator<T>(opts: T): T {
return {
...opts,
};
}
代码很简单,把 key 和对应参数放在 this.service.generators对象里面就结束了。
接下来看看具体使用方法,我们找一个简单的注册方法看一下代码怎么写。
从下面代码可以注册命令也是插件,registerGenerator需要传进来两个核心参数
- 第一是 key,比如这个插件
key:prettier,执行命令就是 umi generator prettier - 第二是 fn,执行 命令时具体要干的事, 会在上文提到的
generator?.fn执行。这里的fn是配置prettier的,具体如下- 找到项目的根目录,在package.json中添加prettier的依赖项
- 找到项目的根目录,添加文件 .prettierrc 和 .prettierignore,并把内容写入
- 执行 npm install ,下载依赖项 。内部用到了
[cross-spawn](https://www.npmjs.com/package/cross-spawn),具体看注释。
import { GeneratorType } from '@umijs/core';
import { logger } from '@umijs/utils';
import { existsSync, writeFileSync } from 'fs';
import { join } from 'path';
import { IApi } from '../../types';
import { GeneratorHelper } from './utils';
export default (api: IApi) => {
api.describe({
key: 'generator:prettier',
});
api.registerGenerator({
key: 'prettier',
name: 'Enable Prettier',
description: 'Setup Prettier Configurations',
type: GeneratorType.enable,
checkEnable: () => {
// 存在 .prettierrc,不开启
return !existsSync(join(api.cwd, '.prettierrc'));
},
disabledDescription:
'prettier has been enabled; You can remove `.prettierrc` to run this again to re-setup.',
fn: async () => {
const h = new GeneratorHelper(api);
// 在package.json中添加prettier的依赖项
h.addDevDeps({
prettier: '^2',
'prettier-plugin-organize-imports': '^2',
'prettier-plugin-packagejson': '^2',
});
// 2、添加 .prettierrc 和 .prettierignore
writeFileSync(
join(api.cwd, '.prettierrc'),
`
{
"printWidth": 80,
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
}
`.trimLeft(),
);
logger.info('Write .prettierrc');
writeFileSync(
join(api.cwd, '.prettierignore'),
`
node_modules
.umi
.umi-production
`.trimLeft(),
);
logger.info('Write .prettierignore');
// 执行 npm install ,下载依赖项
// const spawn = require('cross-spawn');
// spawn('npm', ['install'], { stdio: 'inherit', cwd: 'xxx' });
h.installDeps();
},
});
};
还有大量命令都是在这个阶段注册进来的,正是因为注册了大量命令,我们可以直接使用
export type { UmiApiRequest, UmiApiResponse } from './features/apiRoute';
export type { IApi, IConfig, IRoute, webpack } from './types';
export default () => {
return {
plugins: [
// registerMethods
require.resolve('./registerMethods'),
// features
process.env.DID_YOU_KNOW !== 'none' &&
require.resolve('@umijs/did-you-know/dist/plugin'),
require.resolve('./features/404/404'),
require.resolve('./features/appData/appData'),
require.resolve('./features/check/check'),
require.resolve('./features/clientLoader/clientLoader'),
require.resolve('./features/codeSplitting/codeSplitting'),
require.resolve('./features/configPlugins/configPlugins'),
require.resolve('./features/crossorigin/crossorigin'),
require.resolve('./features/depsOnDemand/depsOnDemand'),
require.resolve('./features/devTool/devTool'),
require.resolve('./features/esmi/esmi'),
require.resolve('./features/exportStatic/exportStatic'),
require.resolve('./features/favicons/favicons'),
require.resolve('./features/mock/mock'),
require.resolve('./features/mpa/mpa'),
require.resolve('./features/overrides/overrides'),
require.resolve('./features/polyfill/polyfill'),
require.resolve('./features/polyfill/publicPathPolyfill'),
require.resolve('./features/routePrefetch/routePrefetch'),
require.resolve('./features/ssr/ssr'),
require.resolve('./features/terminal/terminal'),
require.resolve('./features/tmpFiles/tmpFiles'),
require.resolve('./features/tmpFiles/configTypes'),
require.resolve('./features/transform/transform'),
require.resolve('./features/lowImport/lowImport'),
require.resolve('./features/vite/vite'),
require.resolve('./features/apiRoute/apiRoute'),
require.resolve('./features/monorepo/redirect'),
require.resolve('./features/test/test'),
require.resolve('./features/clickToComponent/clickToComponent'),
require.resolve('./features/legacy/legacy'),
require.resolve('./features/classPropertiesLoose/classPropertiesLoose'),
// commands
require.resolve('./commands/build'),
require.resolve('./commands/config/config'),
require.resolve('./commands/dev/dev'),
require.resolve('./commands/help'),
require.resolve('./commands/lint'),
require.resolve('./commands/setup'),
require.resolve('./commands/version'),
require.resolve('./commands/generators/page'),
require.resolve('./commands/generators/prettier'),
require.resolve('./commands/generators/tsconfig'),
require.resolve('./commands/generators/jest'),
require.resolve('./commands/generators/tailwindcss'),
require.resolve('./commands/generators/dva'),
require.resolve('./commands/generators/component'),
require.resolve('./commands/generators/mock'),
require.resolve('./commands/generators/cypress'),
require.resolve('./commands/generators/api'),
require.resolve('./commands/plugin'),
require.resolve('./commands/verify-commit'),
require.resolve('./commands/preview'),
require.resolve('@umijs/plugin-run'),
].filter(Boolean),
};
};
总结
通过前面几个章节的学习,我们发现很多事情都是围绕着packages/core/src/service/pluginAPI.ts中的方法展开的。
具体方法如下
describe注册插件时的描述信息registerGenerator命令生成器,扩展命令用的,需要fn和key可以通过umi generator key执行注册的命令 fn- registerCommand 注册命令用的,
umi dev、umi build都是通过这个命令注册的 - 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时,注册到
至此项目组架构性质的代码已经梳理的差不多了,通过这几篇文档的总结,大致能看出 umi 整体架构的实现思路以及各种方法实现思路。整个系统通过 核心的 pluginAPI 和 service 很好的关联在了一起。