启动过程

  • 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); } } }

  1. - 开发环境执行的dev方法
  2. ```typescript
  3. import fork from './fork';
  4. export function dev() {
  5. // fork一个子进程并设置scriptPath
  6. const child = fork({
  7. scriptPath: require.resolve('../../bin/forkedDev'),
  8. });
  9. // ref:
  10. // http://nodejs.cn/api/process/signal_events.html
  11. // https://lisk.io/blog/development/why-we-stopped-using-npm-start-child-processes
  12. // 监听当主进程退出时,手动结束掉子进程
  13. process.on('SIGINT', () => {
  14. child.kill('SIGINT');
  15. // ref:
  16. // https://github.com/umijs/umi/issues/6009
  17. process.exit(0);
  18. });
  19. process.on('SIGTERM', () => {
  20. child.kill('SIGTERM');
  21. process.exit(1);
  22. });
  23. }
  • foredDev文件

    1. (async () => {
    2. try {
    3. // 解析参数
    4. const args = yParser(process.argv.slice(2));
    5. // 初始化Service实例
    6. const service = new Service();
    7. // 执行run2方法
    8. await service.run2({
    9. name: DEV_COMMAND,
    10. args,
    11. });
    12. // 监听退出相关事件
    13. let closed = false;
    14. // kill(2) Ctrl-C
    15. process.once('SIGINT', () => onSignal('SIGINT'));
    16. // kill(3) Ctrl-\
    17. process.once('SIGQUIT', () => onSignal('SIGQUIT'));
    18. // kill(15) default
    19. process.once('SIGTERM', () => onSignal('SIGTERM'));
    20. function onSignal(signal: string) {
    21. if (closed) return;
    22. closed = true;
    23. // 退出时触发插件中的 onExit 事件
    24. service.applyPlugins({
    25. key: 'onExit',
    26. args: {
    27. signal,
    28. },
    29. });
    30. process.exit(0);
    31. }
    32. } catch (e: any) {
    33. logger.error(e);
    34. process.exit(1);
    35. }
    36. })();

    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({

    1. ...opts,
    2. // 环境变量
    3. env: process.env.NODE_ENV,
    4. // 当前工作目录
    5. cwd,
    6. // 默认配置文件
    7. defaultConfigFiles: DEFAULT_CONFIG_FILES,
    8. frameworkName: FRAMEWORK_NAME,
    9. // 预设
    10. presets: [require.resolve('@umijs/preset-umi'), ...(opts?.presets || [])],
    11. // 插件
    12. plugins: [
    13. existsSync(join(cwd, 'plugin.ts')) && join(cwd, 'plugin.ts'),
    14. existsSync(join(cwd, 'plugin.js')) && join(cwd, 'plugin.js'),
    15. ].filter(Boolean),

    }); } async run2(opts: { name: string; args?: any }) { let name = opts.name; if (opts?.args.version || name === ‘v’) {

    1. name = 'version';

    } else if (opts?.args.help || !name || name === ‘h’) {

    1. name = 'help';

    } // 执行CoreService类中的run方法 return await this.run({ …opts, name }); } }

  1. - CoreService来自于@umijs/core库中src/service/service.ts
  2. ```typescript
  3. async run(opts: { name: string; args?: any }) {
  4. const { name, args = {} } = opts;
  5. args._ = args._ || [];
  6. // shift the command itself
  7. if (args._[0] === name) args._.shift();
  8. this.args = args;
  9. this.name = name;
  10. // 当前umi阶段为init阶段
  11. this.stage = ServiceStage.init;
  12. // 加载工作目录下.env文件中的环境变量并设置到process.env上
  13. loadEnv({ cwd: this.cwd, envFile: '.env' });
  14. // 获取工作目录中package.json信息及package.json路径并挂载至this上
  15. let pkg: Record<string, string | Record<string, any>> = {};
  16. let pkgPath: string = '';
  17. try {
  18. pkg = require(join(this.cwd, 'package.json'));
  19. pkgPath = join(this.cwd, 'package.json');
  20. } catch (_e) {
  21. // APP_ROOT
  22. if (this.cwd !== process.cwd()) {
  23. try {
  24. pkg = require(join(process.cwd(), 'package.json'));
  25. pkgPath = join(process.cwd(), 'package.json');
  26. } catch (_e) {}
  27. }
  28. }
  29. this.pkg = pkg;
  30. this.pkgPath = pkgPath;
  31. // 获取用户配置信息
  32. // 1. 获取用户配置文件并挂载到config实例上
  33. const configManager = new Config({
  34. cwd: this.cwd,
  35. env: this.env,
  36. defaultConfigFiles: this.opts.defaultConfigFiles,
  37. });
  38. // 用户配置挂载至Services类上
  39. this.configManager = configManager;
  40. this.userConfig = configManager.getUserConfig().config;
  41. // 初始化预设及插件
  42. const { plugins, presets } = Plugin.getPluginsAndPresets({
  43. cwd: this.cwd,
  44. pkg,
  45. plugins: [require.resolve('./generatePlugin')].concat(
  46. this.opts.plugins || [],
  47. ),
  48. presets: [require.resolve('./servicePlugin')].concat(
  49. this.opts.presets || [],
  50. ),
  51. userConfig: this.userConfig,
  52. prefix: this.opts.frameworkName || DEFAULT_FRAMEWORK_NAME,
  53. });
  54. // 当前umi阶段为initPresets阶段
  55. this.stage = ServiceStage.initPresets;
  56. const presetPlugins: Plugin[] = [];
  57. // 注册预设及插件
  58. // 循环初始化预设,preset插入到队首,plugins插入到队尾,主要保证执行的顺序和关系
  59. while (presets.length) {
  60. await this.initPreset({
  61. preset: presets.shift()!,
  62. presets,
  63. plugins: presetPlugins,
  64. });
  65. }
  66. plugins.unshift(...presetPlugins);
  67. // 当前umi阶段为initPlugins阶段
  68. this.stage = ServiceStage.initPlugins;
  69. // 循环初始化plugin
  70. while (plugins.length) {
  71. await this.initPlugin({ plugin: plugins.shift()!, plugins });
  72. }
  73. // 生成plugin map对象
  74. for (const id of Object.keys(this.plugins)) {
  75. this.keyToPluginMap[this.plugins[id].key] = this.plugins[id];
  76. }
  77. // 收集相关默认配置
  78. for (const id of Object.keys(this.plugins)) {
  79. const { config, key } = this.plugins[id];
  80. if (config.schema) this.configSchemas[key] = config.schema;
  81. if (config.default !== undefined) {
  82. this.configDefaults[key] = config.default;
  83. }
  84. this.configOnChanges[key] = config.onChange || ConfigChangeType.reload;
  85. }
  86. // 获取相关path路径
  87. const paths = getPaths({
  88. cwd: this.cwd,
  89. env: this.env,
  90. prefix: this.opts.frameworkName || DEFAULT_FRAMEWORK_NAME,
  91. });
  92. // 重新定义输出路径
  93. if (this.config.outputPath) {
  94. paths.absOutputPath = join(this.cwd, this.config.outputPath);
  95. }
  96. // 当前umi阶段为resolveConfig阶段
  97. this.stage = ServiceStage.resolveConfig;
  98. const config = await this.applyPlugins({
  99. key: 'modifyConfig',
  100. initialValue: lodash.cloneDeep(
  101. configManager.getConfig({
  102. schemas: this.configSchemas,
  103. }).config,
  104. ),
  105. args: { paths },
  106. });
  107. const defaultConfig = await this.applyPlugins({
  108. key: 'modifyDefaultConfig',
  109. initialValue: this.configDefaults,
  110. });
  111. // 合并默认配置以及用户配置
  112. this.config = lodash.merge(defaultConfig, config) as Record<string, any>;
  113. this.paths = await this.applyPlugins({
  114. key: 'modifyPaths',
  115. initialValue: paths,
  116. });
  117. // 当前umi阶段为collectAppData阶段
  118. this.stage = ServiceStage.collectAppData;
  119. // 收集app相关数据信息
  120. this.appData = await this.applyPlugins({
  121. key: 'modifyAppData',
  122. initialValue: {
  123. // base
  124. cwd: this.cwd,
  125. pkg,
  126. pkgPath,
  127. plugins,
  128. presets,
  129. name,
  130. args,
  131. // config
  132. userConfig: this.userConfig,
  133. mainConfigFile: configManager.mainConfigFile,
  134. config,
  135. defaultConfig: defaultConfig,
  136. // TODO
  137. // moduleGraph,
  138. // routes,
  139. // npmClient,
  140. // nodeVersion,
  141. // gitInfo,
  142. // gitBranch,
  143. // debugger info,
  144. // devPort,
  145. // devHost,
  146. // env
  147. },
  148. });
  149. // 当前umi阶段为onCheck阶段
  150. this.stage = ServiceStage.onCheck;
  151. await this.applyPlugins({
  152. key: 'onCheck',
  153. });
  154. // 当前umi阶段为onStart阶段
  155. this.stage = ServiceStage.onStart;
  156. await this.applyPlugins({
  157. key: 'onStart',
  158. });
  159. // 当前umi阶段为runCommand阶段
  160. this.stage = ServiceStage.runCommand;
  161. const command = this.commands[name];
  162. assert(command, `Invalid command ${name}, it's not registered.`);
  163. return command.fn({ args });
  164. }

run中生命周期:

  • init stage
  • initPresets stage
  • initPlugins stage
  • resolveConfig stage
  • collectAppData stage
  • onCheck stage
  • onStart stage
  • runCommand stage

    init stage

  • Service 通过调用 Plugin.getPluginsAndPresets 静态方法来获取所有的插件

    1. static getPluginsAndPresets(opts: {
    2. cwd: string;
    3. pkg: any;
    4. userConfig: any;
    5. plugins?: string[];
    6. presets?: string[];
    7. prefix: string;
    8. }) {
    9. function get(type: 'plugin' | 'preset') {
    10. const types = `${type}s` as 'plugins' | 'presets';
    11. return [
    12. // opts中,在调用 Plugin.getPluginsAndPresets 时可以向其传入部分插件
    13. ...(opts[types] || []),
    14. // env 中,环境变量里可以添加插件
    15. ...(process.env[`${opts.prefix}_${types}`.toUpperCase()] || '')
    16. .split(',')
    17. .filter(Boolean),
    18. // 从用户配置中获取环境变量
    19. ...(opts.userConfig[types] || []),
    20. ].map((path) => {
    21. assert(
    22. typeof path === 'string',
    23. `Invalid plugin ${path}, it must be string.`,
    24. );
    25. let resolved;
    26. // 校验path是否存在
    27. try {
    28. resolved = resolve.sync(path, {
    29. basedir: opts.cwd,
    30. extensions: ['.tsx', '.ts', '.mjs', '.jsx', '.js'],
    31. });
    32. } catch (_e) {
    33. throw new Error(`Invalid plugin ${path}, can not be resolved.`);
    34. }
    35. return new Plugin({
    36. path: resolved,
    37. type,
    38. cwd: opts.cwd,
    39. });
    40. });
    41. }
    42. return {
    43. presets: get('preset'),
    44. plugins: get('plugin'),
    45. };
    46. }

    Plugin类

    从上面代码可以看到,在init阶段获取所有的插件时,实例化Plugin类

    1. export class Plugin {
    2. constructor(opts: IOpts) {
    3. this.type = opts.type;
    4. // 将文件路径转换为兼容 window 的路径
    5. this.path = winPath(opts.path);
    6. this.cwd = opts.cwd;
    7. // 检验路径是否存在
    8. assert(
    9. existsSync(this.path),
    10. `Invalid ${this.type} ${this.path}, it's not exists.`,
    11. );
    12. let pkg = null;
    13. let isPkgEntry = false;
    14. // 查找最近的 package.json 文件
    15. const pkgJSONPath = pkgUp.pkgUpSync({ cwd: this.path })!;
    16. if (pkgJSONPath) {
    17. // 加载插件package.json文件
    18. pkg = require(pkgJSONPath);
    19. // package.json入口文件是否一致
    20. isPkgEntry =
    21. winPath(join(dirname(pkgJSONPath), pkg.main || 'index.js')) ===
    22. winPath(this.path);
    23. }
    24. this.id = this.getId({ pkg, isPkgEntry, pkgJSONPath });
    25. this.key = this.getKey({ pkg, isPkgEntry });
    26. this.apply = () => {
    27. // 注册require加载器
    28. register.register({
    29. implementor: esbuild,
    30. });
    31. register.clearFiles();
    32. let ret;
    33. try {
    34. // 加载插件代码
    35. ret = require(this.path);
    36. } catch (e: any) {
    37. throw new Error(
    38. `Register ${this.type} ${this.path} failed, since ${e.message}`,
    39. );
    40. }
    41. // 删除require文件缓存
    42. for (const file of register.getFiles()) {
    43. delete require.cache[file];
    44. }
    45. register.restore();
    46. // 根据插件打包后的格式进行返回
    47. return ret.__esModule ? ret.default : ret;
    48. };
    49. }
    50. }