umi源码-02从一个命令说起 umi dev

248 阅读3分钟

umi dev

umi 命令在 scripts/umi文件夹下面,我们先看 umi dev 命令。 先来看一下目录结构,有个大致的印象 image.png

run

命令的核心是一个 run 方法

// bin/umi.js

#!/usr/bin/env node

// disable since it's conflicted with typescript cjs + dynamic import
// require('v8-compile-cache');

// patch console for debug
// ref: https://remysharp.com/2014/05/23/where-is-that-console-log
if (process.env.DEBUG_CONSOLE) {
  ['log', 'warn', 'error'].forEach((method) => {
    const old = console[method];
    console[method] = function () {
      let stack = new Error().stack.split(/\n/);
      // Chrome includes a single "Error" line, FF doesn't.
      if (stack[0].indexOf('Error') === 0) {
        stack = stack.slice(1);
      }
      const args = [].slice.apply(arguments).concat([stack[1].trim()]);
      return old.apply(console, args);
    };
  });
}

require('../dist/cli/cli')
  .run()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  });

这个run方法就是 src/cli/cli.ts 中的方法,这个方法最核心的就是 dev()方法,当然还有 new Service.run2() ,此处我们暂时只看 dev() 方法,new Service.run2()方法会在后面章节介绍。

// src/cli/cli.ts
import { logger, printHelp, 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) {
  checkNodeVersion();
  checkLocal();
  setNodeTitle();
  setNoDeprecation();

  const args = yParser(process.argv.slice(2), {
    alias: {
      version: ['v'],
      help: ['h'],
    },
    boolean: ['version'],
  });
  const command = args._[0];
  if ([DEV_COMMAND, '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();
  } else {
    try {
      await new Service().run2({
        name: args._[0],
        args,
      });
    } catch (e: any) {
      logger.fatal(e);
      printHelp.exit();
      process.exit(1);
    }
  }
}

dev

dev中的核心其实就一个 fork方法,这里的fork其实就是 child_process的fork,fork进来的路径对应的文件是一个自执行函数,我们先看这个fork的核心逻辑,下一步再看具体的执行思路。

// src/cli/dev.ts
import fork from './fork';
const child = fork({
  scriptPath: require.resolve('../../bin/forkedDev'),
});

// src/cli/fork.ts
import { fork } from 'child_process';
const child = fork(scriptPath, process.argv.slice(2), { execArgv });


forkDev

通过这个forkDev.ts 文件,我们可以这里实例化了一个服务,执行了服务中注册的run2方法。下一步我们重点看一下 service.run2 方法

// src/cli/forkedDev.ts
import { logger, printHelp, yParser } from '@umijs/utils';
import { DEV_COMMAND, FRAMEWORK_NAME } from '../constants';
import { Service } from '../service/service';
import { setNoDeprecation, setNodeTitle } from './node';
setNodeTitle(`${FRAMEWORK_NAME}-dev`);
setNoDeprecation();

(async () => {
  try {
    // 获取命令行中输入的参数
    const args = yParser(process.argv.slice(2));
    // 实例化启动命令的服务
    const service = new Service();
    // 执行服务中的 run2 方法,
    await service.run2({
      name: DEV_COMMAND,
      args,
    });
   // ... 省略其他非核心逻辑
})();

service

这个文件两段核心代码是 super()this.run()。其中,super是为了初始化一些基础配置、插件、插件集合。this.run() 是真正开始启动项目的流程。通过下面代码可以看到,Service中并没用 run 方法,但是 Service 继承了 CoreService,在 CoreService 中我们会看到 run 方法,这个我们会在下一篇文章中介绍。

// src/service/service.ts
import { Service as CoreService } from '@umijs/core';
import { existsSync } from 'fs';
import { dirname, join } from 'path';
import { DEFAULT_CONFIG_FILES, FRAMEWORK_NAME } from '../constants';
import { getCwd } from './cwd';

export class Service extends CoreService {
  constructor(opts?: any) {
    process.env.UMI_DIR = dirname(require.resolve('../../package'));
    const cwd = getCwd();
    // Why?
    // plugin import from umi but don't explicitly depend on it
    // and we may also have old umi installed
    // ref: https://github.com/umijs/umi/issues/8342#issuecomment-1182654076
    require('./requireHook');
    super({
      ...opts,
      env: process.env.NODE_ENV,
      cwd,
      // 这里是用户的配置文件枚举,可以是以下四种中的一种
      // '.umirc.ts','.umirc.js','config/config.ts','config/config.js',
      defaultConfigFiles: opts?.defaultConfigFiles || DEFAULT_CONFIG_FILES,
      frameworkName: FRAMEWORK_NAME,
      // 这里是插件集合,默认添加了@umijs/preset-umi插件集合
      // 也可以通过命令行配置 例如:umi dev presets=@plugin-xxx
      presets: [require.resolve('@umijs/preset-umi'), ...(opts?.presets || [])],
      // 这里是用户配置的插件文件,可以是plugin.ts,也可以是 plugin.js。
      // 可以简单理解为根目录下有一个 plugin.ts或者 plugin.js文件。
      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';
    }

    // TODO
    // initWebpack

    return await this.run({ ...opts, name });
  }
}

补充:因为dev命令执行的链路非常长,本篇文章只说到的 service,后面我会接着 service 继续做具体说明