启动过程
- 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方法```typescriptimport fork from './fork';export function dev() {// fork一个子进程并设置scriptPathconst 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/6009process.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-Cprocess.once('SIGINT', () => onSignal('SIGINT'));// kill(3) Ctrl-\process.once('SIGQUIT', () => onSignal('SIGQUIT'));// kill(15) defaultprocess.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```typescriptasync run(opts: { name: string; args?: any }) {const { name, args = {} } = opts;args._ = args._ || [];// shift the command itselfif (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_ROOTif (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;// 循环初始化pluginwhile (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: {// basecwd: this.cwd,pkg,pkgPath,plugins,presets,name,args,// configuserConfig: 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;};}}
