umi源码-03run方法获取配置文件信息

578 阅读1分钟

这一篇衔接了上一篇的 继承 Service 并执行其中的 run 方法, Service 的代码在 umi/packages/core/src/service/service.ts 中,我们以其中的 run 方法为入口抽丝剥茧,详细介绍。

代码太长,这里放一个链接,方便大家查看,下面从 run 方法中具体执行的事情一一说起。 github.com/umijs/umi/b…

  • 获取 环境变量 .env 或者 .env.local,逻辑比较简单,主要有三步
    • 读取配置文件内容并解析, readFileSync & parse
    • 解析配置文件为json格式,expand
    • 在process.env中添加读取到的配置项 process.env[key] = parsed[key]
 // loadEnv({ cwd: this.cwd, envFile: '.env' });

import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { parse } from '../../compiled/dotenv';
import { expand } from '../../compiled/dotenv-expand';

export function loadEnv(opts: { cwd: string; envFile: string }) {
  const files = [
    join(opts.cwd, opts.envFile),
    join(opts.cwd, `${opts.envFile}.local`),
  ];
  for (const file of files) {
    if (!existsSync(file)) continue;
    const parsed: Record<string, string> = parse(readFileSync(file)) || {};
    expand({ parsed, ignoreProcessEnv: true });
    for (const key of Object.keys(parsed)) {
      process.env[key] = parsed[key];
    }
  }
}

  • 读取命令执行所在的根路径对应的 package.json 内容和路径,代码也比较简单
this.pkg = require(join(this.cwd, 'package.json'));
this.pkgPath = join(this.cwd, 'package.json');
  • 实例化用户的基础配置

这里先说一下静态方法的一些特点,后面会用到,静态方法只能通过类调用,不能通过实例调用。

class Foo {
	static staticMethod() {
		return 'hello';
	}
  basicMethod() {
    return 'basic'
  }
}
// 静态方法只能通过类调用
Foo.staticMethod() // 'hello'
foo.staticMethod()
	// TypeError: foo.classMethod is not a function

var foo = new Foo();
// 非静态方法,可以通过实例调用
foo.basicMethod() // 'basic'

  • 获取 .umirc.ts 中的配置内容,以及其他配置信息
const configManager = new Config({
  cwd: this.cwd,
  env: this.env,
  defaultConfigFiles: this.opts.defaultConfigFiles,
  specifiedEnv,
});

this.configManager = configManager;
// 获取配置的关键代码
this.userConfig = configManager.getUserConfig().config;

configManager.getUserConfig().config; 整个函数执行流程如下:

  • getUserConfig
  • static getConfigFiles 获取 .umirc.ts 、.umirc.test.ts 等文件名
  • static getUserConfig 获取 .umirc.ts 里面的内容,用到了 esbuild 解析文件内容
  • 删除了非必要代码的配置文件如下,具体请看注释
import esbuild from '@umijs/bundler-utils/compiled/esbuild';
import { chokidar, lodash, register, semver } from '@umijs/utils';
import joi from '@umijs/utils/compiled/@hapi/joi';
import assert from 'assert';
import { existsSync } from 'fs';
import { join } from 'path';
import { diff } from '../../compiled/just-diff';
import {
  DEFAULT_CONFIG_FILES,
  LOCAL_EXT,
  SHORT_ENV,
  WATCH_DEBOUNCE_STEP,
} from '../constants';
import { Env } from '../types';
import { addExt, getAbsFiles } from './utils';

interface IOpts {
  cwd: string;
  env: Env;
  specifiedEnv?: string;
  defaultConfigFiles?: string[];
}

type ISchema = Record<string, any>;

type IOnChangeTypes = Record<string, string | Function>;

export class Config {
  public opts: IOpts;
  public mainConfigFile: string | null;
  public prevConfig: any;
  public files: string[] = [];
  constructor(opts: IOpts) {
    this.opts = opts;
    // 获取配置文件类似,.umirc.ts
    this.mainConfigFile = Config.getMainConfigFile(this.opts);
    this.prevConfig = null;
  }

  getUserConfig() {
    // .umirc.ts => .umirc.ts
    // .umirc.ts => .umirc.test.ts
    // .umirc.ts => .umirc.production.ts
    const configFiles = Config.getConfigFiles({
      mainConfigFile: this.mainConfigFile,
      env: this.opts.env,
      specifiedEnv: this.opts.specifiedEnv,
    });

    return Config.getUserConfig({
      configFiles: getAbsFiles({
        files: configFiles,
        cwd: this.opts.cwd,
      }),
    });
  }

  // 获取配置文件类似,.umirc.ts,只要找到第一个就不再往下找,默认是 .umirc.ts
  static getMainConfigFile(opts: {
    cwd: string;
    defaultConfigFiles?: string[];
  }) {
    let mainConfigFile = null;
    for (const configFile of opts.defaultConfigFiles || DEFAULT_CONFIG_FILES) {
      const absConfigFile = join(opts.cwd, configFile);
      if (existsSync(absConfigFile)) {
        mainConfigFile = absConfigFile;
        break;
      }
    }
    return mainConfigFile;
  }

  static getConfigFiles(opts: {
    mainConfigFile: string | null;
    env: Env;
    specifiedEnv?: string;
  }) {
    const ret: string[] = [];
    const { mainConfigFile } = opts;
    const specifiedEnv = opts.specifiedEnv || '';
    if (mainConfigFile) {
      const env = SHORT_ENV[opts.env] || opts.env;
      ret.push(
        ...[
          mainConfigFile,
          specifiedEnv &&
            addExt({ file: mainConfigFile, ext: `.${specifiedEnv}` }),
          addExt({ file: mainConfigFile, ext: `.${env}` }),
          specifiedEnv &&
            addExt({
              file: mainConfigFile,
              ext: `.${env}.${specifiedEnv}`,
            }),
        ].filter(Boolean),
      );

      if (opts.env === Env.development) {
        ret.push(addExt({ file: mainConfigFile, ext: LOCAL_EXT }));
      }
    }
    return ret;
  }

  // 解析 .umirc.ts 获取 配置信息 json 数据
  static getUserConfig(opts: { configFiles: string[] }) {
    let config = {};
    let files: string[] = [];

    for (const configFile of opts.configFiles) {
      if (existsSync(configFile)) {
        register.register({
          implementor: esbuild,
        });
        register.clearFiles();
        try {
          config = lodash.merge(config, require(configFile).default);
        } catch (e) {
          // Error.prototype.cause has been fully supported from  node v16.9.0
          // Ref https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause#browser_compatibility
          if (semver.lt(semver.clean(process.version)!, '16.9.0')) {
            throw e;
          }

          // @ts-ignore
          throw new Error(`Parse config file failed: [${configFile}]`, {
            cause: e,
          });
        }
        for (const file of register.getFiles()) {
          delete require.cache[file];
        }
        // includes the config File
        files.push(...register.getFiles());
        register.restore();
      } else {
        files.push(configFile);
      }
    }

    return {
      config,
      files,
    };
  }
}