启动过程
- Umi 命令会执行 bin/umi.js 文件,该文件直接运行 cli/cli.ts 中的
run()
方法 ```javascript import { logger, yParser } from ‘@umijs/utils’; import { DEV_COMMAND } from ‘../constants’; import { Service } from ‘../service/service’; import { dev } from ‘./dev’; import { checkLocal, checkVersion as checkNodeVersion, setNoDeprecation, setNodeTitle, } from ‘./node’;
interface IOpts { presets?: string[]; }
export async function run(opts?: IOpts) { // 检查node环境 checkNodeVersion(); // 检查是否是本地 checkLocal(); // 设置Node title setNodeTitle(); // 设置是否将弃用警告消息打印到stderr setNoDeprecation(); // 解析命令行参数 const args = yParser(process.argv.slice(2), { alias: { version: [‘v’], help: [‘h’], }, boolean: [‘version’], });
// 设置环境变更nodeenv为开发或者生产环境 const command = args.[0]; if ([DEVCOMMAND, ‘setup’].includes(command)) { process.env.NODE_ENV = ‘development’; } else if (command === ‘build’) { process.env.NODE_ENV = ‘production’; } // 判断是否存在预设 if (opts?.presets) { process.env.UMI_PRESETS = opts.presets.join(‘,’); } // 判断开发和生产环境下执行不同的命令 if (command === DEV_COMMAND) { // 执行dev方法 dev(); } else { try { // 生产环境直接实例化Service类并执行run2方法 await new Service().run2({ name: args.[0], args, }); } catch (e: any) { logger.error(e); process.exit(1); } } }
- 开发环境执行的dev方法
```typescript
import fork from './fork';
export function dev() {
// fork一个子进程并设置scriptPath
const child = fork({
scriptPath: require.resolve('../../bin/forkedDev'),
});
// ref:
// http://nodejs.cn/api/process/signal_events.html
// https://lisk.io/blog/development/why-we-stopped-using-npm-start-child-processes
// 监听当主进程退出时,手动结束掉子进程
process.on('SIGINT', () => {
child.kill('SIGINT');
// ref:
// https://github.com/umijs/umi/issues/6009
process.exit(0);
});
process.on('SIGTERM', () => {
child.kill('SIGTERM');
process.exit(1);
});
}
foredDev文件
(async () => {
try {
// 解析参数
const args = yParser(process.argv.slice(2));
// 初始化Service实例
const service = new Service();
// 执行run2方法
await service.run2({
name: DEV_COMMAND,
args,
});
// 监听退出相关事件
let closed = false;
// kill(2) Ctrl-C
process.once('SIGINT', () => onSignal('SIGINT'));
// kill(3) Ctrl-\
process.once('SIGQUIT', () => onSignal('SIGQUIT'));
// kill(15) default
process.once('SIGTERM', () => onSignal('SIGTERM'));
function onSignal(signal: string) {
if (closed) return;
closed = true;
// 退出时触发插件中的 onExit 事件
service.applyPlugins({
key: 'onExit',
args: {
signal,
},
});
process.exit(0);
}
} catch (e: any) {
logger.error(e);
process.exit(1);
}
})();
Service类
从上述启动过程来看,主要核心还是在Service类中 ```typescript export class Service extends CoreService { constructor(opts?: any) { // 设置UMI_DIR目录 process.env.UMI_DIR = dirname(require.resolve(‘../../package’)); // 获取当前工作目录 const cwd = getCwd(); // 调用super函数传入相关配置信息 super({
...opts,
// 环境变量
env: process.env.NODE_ENV,
// 当前工作目录
cwd,
// 默认配置文件
defaultConfigFiles: DEFAULT_CONFIG_FILES,
frameworkName: FRAMEWORK_NAME,
// 预设
presets: [require.resolve('@umijs/preset-umi'), ...(opts?.presets || [])],
// 插件
plugins: [
existsSync(join(cwd, 'plugin.ts')) && join(cwd, 'plugin.ts'),
existsSync(join(cwd, 'plugin.js')) && join(cwd, 'plugin.js'),
].filter(Boolean),
}); } async run2(opts: { name: string; args?: any }) { let name = opts.name; if (opts?.args.version || name === ‘v’) {
name = 'version';
} else if (opts?.args.help || !name || name === ‘h’) {
name = 'help';
} // 执行CoreService类中的run方法 return await this.run({ …opts, name }); } }
- CoreService来自于@umijs/core库中src/service/service.ts
```typescript
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;
// 当前umi阶段为init阶段
this.stage = ServiceStage.init;
// 加载工作目录下.env文件中的环境变量并设置到process.env上
loadEnv({ cwd: this.cwd, envFile: '.env' });
// 获取工作目录中package.json信息及package.json路径并挂载至this上
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;
// 获取用户配置信息
// 1. 获取用户配置文件并挂载到config实例上
const configManager = new Config({
cwd: this.cwd,
env: this.env,
defaultConfigFiles: this.opts.defaultConfigFiles,
});
// 用户配置挂载至Services类上
this.configManager = configManager;
this.userConfig = configManager.getUserConfig().config;
// 初始化预设及插件
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: this.opts.frameworkName || DEFAULT_FRAMEWORK_NAME,
});
// 当前umi阶段为initPresets阶段
this.stage = ServiceStage.initPresets;
const presetPlugins: Plugin[] = [];
// 注册预设及插件
// 循环初始化预设,preset插入到队首,plugins插入到队尾,主要保证执行的顺序和关系
while (presets.length) {
await this.initPreset({
preset: presets.shift()!,
presets,
plugins: presetPlugins,
});
}
plugins.unshift(...presetPlugins);
// 当前umi阶段为initPlugins阶段
this.stage = ServiceStage.initPlugins;
// 循环初始化plugin
while (plugins.length) {
await this.initPlugin({ plugin: plugins.shift()!, plugins });
}
// 生成plugin map对象
for (const id of Object.keys(this.plugins)) {
this.keyToPluginMap[this.plugins[id].key] = this.plugins[id];
}
// 收集相关默认配置
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;
}
// 获取相关path路径
const paths = getPaths({
cwd: this.cwd,
env: this.env,
prefix: this.opts.frameworkName || DEFAULT_FRAMEWORK_NAME,
});
// 重新定义输出路径
if (this.config.outputPath) {
paths.absOutputPath = join(this.cwd, this.config.outputPath);
}
// 当前umi阶段为resolveConfig阶段
this.stage = ServiceStage.resolveConfig;
const config = await this.applyPlugins({
key: 'modifyConfig',
initialValue: lodash.cloneDeep(
configManager.getConfig({
schemas: this.configSchemas,
}).config,
),
args: { paths },
});
const defaultConfig = await this.applyPlugins({
key: 'modifyDefaultConfig',
initialValue: this.configDefaults,
});
// 合并默认配置以及用户配置
this.config = lodash.merge(defaultConfig, config) as Record<string, any>;
this.paths = await this.applyPlugins({
key: 'modifyPaths',
initialValue: paths,
});
// 当前umi阶段为collectAppData阶段
this.stage = ServiceStage.collectAppData;
// 收集app相关数据信息
this.appData = await this.applyPlugins({
key: 'modifyAppData',
initialValue: {
// base
cwd: this.cwd,
pkg,
pkgPath,
plugins,
presets,
name,
args,
// config
userConfig: this.userConfig,
mainConfigFile: configManager.mainConfigFile,
config,
defaultConfig: defaultConfig,
// TODO
// moduleGraph,
// routes,
// npmClient,
// nodeVersion,
// gitInfo,
// gitBranch,
// debugger info,
// devPort,
// devHost,
// env
},
});
// 当前umi阶段为onCheck阶段
this.stage = ServiceStage.onCheck;
await this.applyPlugins({
key: 'onCheck',
});
// 当前umi阶段为onStart阶段
this.stage = ServiceStage.onStart;
await this.applyPlugins({
key: 'onStart',
});
// 当前umi阶段为runCommand阶段
this.stage = ServiceStage.runCommand;
const command = this.commands[name];
assert(command, `Invalid command ${name}, it's not registered.`);
return command.fn({ args });
}
run中生命周期:
- init stage
- initPresets stage
- initPlugins stage
- resolveConfig stage
- collectAppData stage
- onCheck stage
- onStart stage
-
init stage
Service
通过调用Plugin.getPluginsAndPresets
静态方法来获取所有的插件static getPluginsAndPresets(opts: {
cwd: string;
pkg: any;
userConfig: any;
plugins?: string[];
presets?: string[];
prefix: string;
}) {
function get(type: 'plugin' | 'preset') {
const types = `${type}s` as 'plugins' | 'presets';
return [
// opts中,在调用 Plugin.getPluginsAndPresets 时可以向其传入部分插件
...(opts[types] || []),
// env 中,环境变量里可以添加插件
...(process.env[`${opts.prefix}_${types}`.toUpperCase()] || '')
.split(',')
.filter(Boolean),
// 从用户配置中获取环境变量
...(opts.userConfig[types] || []),
].map((path) => {
assert(
typeof path === 'string',
`Invalid plugin ${path}, it must be string.`,
);
let resolved;
// 校验path是否存在
try {
resolved = resolve.sync(path, {
basedir: opts.cwd,
extensions: ['.tsx', '.ts', '.mjs', '.jsx', '.js'],
});
} catch (_e) {
throw new Error(`Invalid plugin ${path}, can not be resolved.`);
}
return new Plugin({
path: resolved,
type,
cwd: opts.cwd,
});
});
}
return {
presets: get('preset'),
plugins: get('plugin'),
};
}
Plugin类
从上面代码可以看到,在init阶段获取所有的插件时,实例化Plugin类
export class Plugin {
constructor(opts: IOpts) {
this.type = opts.type;
// 将文件路径转换为兼容 window 的路径
this.path = winPath(opts.path);
this.cwd = opts.cwd;
// 检验路径是否存在
assert(
existsSync(this.path),
`Invalid ${this.type} ${this.path}, it's not exists.`,
);
let pkg = null;
let isPkgEntry = false;
// 查找最近的 package.json 文件
const pkgJSONPath = pkgUp.pkgUpSync({ cwd: this.path })!;
if (pkgJSONPath) {
// 加载插件package.json文件
pkg = require(pkgJSONPath);
// package.json入口文件是否一致
isPkgEntry =
winPath(join(dirname(pkgJSONPath), pkg.main || 'index.js')) ===
winPath(this.path);
}
this.id = this.getId({ pkg, isPkgEntry, pkgJSONPath });
this.key = this.getKey({ pkg, isPkgEntry });
this.apply = () => {
// 注册require加载器
register.register({
implementor: esbuild,
});
register.clearFiles();
let ret;
try {
// 加载插件代码
ret = require(this.path);
} catch (e: any) {
throw new Error(
`Register ${this.type} ${this.path} failed, since ${e.message}`,
);
}
// 删除require文件缓存
for (const file of register.getFiles()) {
delete require.cache[file];
}
register.restore();
// 根据插件打包后的格式进行返回
return ret.__esModule ? ret.default : ret;
};
}
}