UmiJS的相关介绍

1,049 阅读6分钟

什么是UmiJS?

UMI是基于一种微内核架构方式,核心是只保留架构的核心功能,其他需求及服务以插件的形式载入,“即插即用,不用即走”,因而又称为“插件化架构”。简单来说就是"如无必要,勿增实体",只保留最精简、最核心的部分。


umi.jpeg

如上图所示,umi首先会加载用户的配置和插件,然后基于配置或者目录生成一份路由配置,再基于此路由配置,把JS/CSS源码和HTML完整的串联起来。


UmiJS的核心竞争力是什么?

插件机制

umi_plugins.png

UMI本身并不负责任何的业务,开发人员可通过安装不同的插件实现不同的功能,从而实现了框架和业务的解耦。

  • 插件即社区,总不可能一个人把所有的功能都写了。
  • 插件用于满足不同人、场景的不同需求。
  • 插件用于封装复杂逻辑,简化使用。

umi_v2.jpg

如上图所示,首先有三个入口,node的编译时、浏览器的运行时、UMI UI。UMI负责读取配置文件,收集用户配置,然后挂载插件,再去执行具体的命令。每个命令再去执行自己的事情,把整个生命周期串联起来。但是每个生命周期里面具体做的事情都是由插件去实现的。

  • 插件不仅用于"编译时",还作用于"运行时"。

基于路由

umi_v7.png

除了配置式路由,UMI也支持约定式路由。可以把路由想象成一个个的页面,文件系统即路由,减少冗余配置。基于路由还可以去做页面标题的切换、代码的分割、按需编译等等。

配置式是对于现实的低头,而约定式是Umi希望走去的方向,因为他简洁优雅。通过“约定大于配置”的核心理念,让开发者可以更加集中精力于业务开发。

需要注意的是,满足以下任意规则的文件不会被注册为路由。

  • 以 . 或 _ 开头的文件或目录
  • 以 d.ts 结尾的类型定义文件
  • 以 test.tsspec.tse2e.ts 结尾的测试文件(适用于 .js.jsx 和 .tsx 文件)
  • components 和 component 目录
  • utils 和 util 目录
  • 不是 .js.jsx.ts 或 .tsx 文件
  • 文件内容不包含 JSX 元素

HTML是一等公民

umi_v6.png

首先在最下面的依赖层保证技术是不落伍的。在实现层除了有UMI之外,还有插件层,区块层。输出层是通过命令行进行输出。最后通过HTML去打通各种应用场景。

区块市场

  • 可以从0搭建一个ant-design-pro。例如:先去选择一个整体的布局,再去选择一些页面,像列表页面、图表页面等等。

插件集

@umijs/preset-react

针对 react 应用的插件集。

包含:


UmiJS的工作原理

umi_core.png

核心源码在core目录下的Config、Route及Service,而核心中的核心就是Service这个类,其他都是基于它进行的相关扩展与融合。


临时文件夹

.umi 临时目录是整个 Umi 项目的发动机,你的入口文件、路由等等都在这里,这些是由 umi 内部插件及三方插件生成的。

临时文件是 Umi 框架中非常重要的一部分,框架或插件会根据你的代码生成临时文件,这些原来需要放在项目里的脏乱差的部分都被藏在了这里。

├─ src
   ├─ .umi
      ├─ .cache
         ├─ babel-loader
            ├─ ...一些缓存文件
      ├─ core
         ├─ devScript.ts #开发脚本
         ├─ history.ts #历史对象
         ├─ plugin.ts #运行时插件
         ├─ pluginConfig.d.ts #配置插件
         ├─ pluginRegister.ts #注册插件
         ├─ polyfill.ts
         ├─ routes.ts #路由定义 umi会扫描pages目录,根据它的目录结构生成这样的文件
         ├─ umiExports.ts #导出
      ├─ umi.ts #入口文件

实现流程

umi_new_flow.jpg


Service核心代码

Service 代码位于 umi\packages\core\src\Service\Service.ts

Service 继承了 nodeJS events 模块提供的实例 EventEmitter,用于实现自定义事件。

umi_service.jpg

export default class Service extends EventEmitter {
  // 项目根路径
  cwd: string;
  // package.json的绝对路径
  pkg: IPackage;
  // 跳过的插件
  skipPluginIds: Set<string> = new Set<string>();
  // 生命周期执行阶段
  stage: ServiceStage = ServiceStage.uninitialized;
  // 注册命令
  commands: {
    [name: string]: ICommand | string;
  } = {};
  // 解析完的插件
  plugins: {
    [id: string]: IPlugin;
  } = {};
  // 插件方法
  pluginMethods: {
    [name: string]: Function;
  } = {};
  // 初始化插件预设
  initialPresets: IPreset[];
  initialPlugins: IPlugin[];
  // 额外的插件预设
  _extraPresets: IPreset[] = [];
  _extraPlugins: IPlugin[] = [];
  // 用户配置
  userConfig: IConfig;
  configInstance: Config;
  config: IConfig | null = null;
  // babel处理
  babelRegister: BabelRegister;
  // 钩子函数处理
  hooksByPluginId: {
    [id: string]: IHook[];
  } = {};
  hooks: {
    [key: string]: IHook[];
  } = {};
  // 用户配置生成的路径信息
  paths: {
	// 项目根目录
    cwd?: string;
	// node_modules文件目录
    absNodeModulesPath?: string;
	// src目录
    absSrcPath?: string;
	// pages目录
    absPagesPath?: string;
	// dist导出目录
    absOutputPath?: string;
	// 生成的.umi目录
    absTmpPath?: string;
  } = {};
  env: string | undefined;
  ApplyPluginsType = ApplyPluginsType;
  EnableBy = EnableBy;
  ConfigChangeType = ConfigChangeType;
  ServiceStage = ServiceStage;
  args: any;

  constructor(opts: IServiceOpts) {
    super();
    this.cwd = opts.cwd || process.cwd();
    // 仓库根目录,antd pro构建的时候需要一个新的空文件夹
    this.pkg = opts.pkg || this.resolvePackage();
    this.env = opts.env || process.env.NODE_ENV;

    // babel处理
    this.babelRegister = new BabelRegister();

    // 加载环境变量
    this.loadEnv();

    // 获取用户配置
    this.configInstance = new Config({
      cwd: this.cwd,
      service: this,
      localConfig: this.env === 'development',
    });

	// 从.umirc.ts中获取内容
    this.userConfig = this.configInstance.getUserConfig();

    // 获取导出的配置
    this.paths = getPaths({
      cwd: this.cwd,
      config: this.userConfig!,
      env: this.env,
    });

    // 初始化插件
    const baseOpts = {
      pkg: this.pkg,
      cwd: this.cwd,
    };

	// 初始化预设
    this.initialPresets = resolvePresets({
      ...baseOpts,
      presets: opts.presets || [],
      userConfigPresets: this.userConfig.presets || [],
    });

	// 初始化插件
    this.initialPlugins = resolvePlugins({
      ...baseOpts,
      plugins: opts.plugins || [],
      userConfigPlugins: this.userConfig.plugins || [],
    });

	// 初始化配置及插件放入babel注册中
    this.babelRegister.setOnlyMap({
      key: 'initialPlugins',
      value: lodash.uniq([
        ...this.initialPresets.map(({ path }) => path),
        ...this.initialPlugins.map(({ path }) => path),
      ]),
    });
  }
  // 设置生命周期
  setStage(stage: ServiceStage) {
    this.stage = stage;
  }
  // 解析package.json的文件
  resolvePackage() {
    try {
      return require(join(this.cwd, 'package.json'));
    } catch (e) {
      return {};
    }
  }
  // 加载环境
  loadEnv() {
    const basePath = join(this.cwd, '.env');
    const localPath = `${basePath}.local`;
    loadDotEnv(basePath);
    loadDotEnv(localPath);
  }

  // 真正的初始化
  async init() {
    this.setStage(ServiceStage.init);
    await this.initPresetsAndPlugins();

	// 状态:初始
    this.setStage(ServiceStage.initHooks);

	// 注册了plugin要执行的钩子方法
    Object.keys(this.hooksByPluginId).forEach((id) => {
      const hooks = this.hooksByPluginId[id];
      hooks.forEach((hook) => {
        const { key } = hook;
        hook.pluginId = id;
        this.hooks[key] = (this.hooks[key] || []).concat(hook);
      });
    });

    // 状态:插件已注册
    this.setStage(ServiceStage.pluginReady);
	// 执行插件
    await this.applyPlugins({
      key: 'onPluginReady',
      type: ApplyPluginsType.event,
    });

    // 状态:获取配置信息
    this.setStage(ServiceStage.getConfig);

	// 拿到对应插件的默认配置信息
    const defaultConfig = await this.applyPlugins({
      key: 'modifyDefaultConfig',
      type: this.ApplyPluginsType.modify,
      initialValue: await this.configInstance.getDefaultConfig(),
    });

	// 将实例中的配置信息对应修改的配置信息
    this.config = await this.applyPlugins({
      key: 'modifyConfig',
      type: this.ApplyPluginsType.modify,
      initialValue: this.configInstance.getConfig({
        defaultConfig,
      }) as any,
    });

    // 状态:合并路径
    this.setStage(ServiceStage.getPaths);
    
    if (this.config!.outputPath) {
      this.paths.absOutputPath = join(this.cwd, this.config!.outputPath);
    }

	// 修改路径对象
    const paths = (await this.applyPlugins({
      key: 'modifyPaths',
      type: ApplyPluginsType.modify,
      initialValue: this.paths,
    })) as object;
    Object.keys(paths).forEach((key) => {
      this.paths[key] = paths[key];
    });
  }

  
  async initPresetsAndPlugins() {
    this.setStage(ServiceStage.initPresets);
    this._extraPlugins = [];
    while (this.initialPresets.length) {
      await this.initPreset(this.initialPresets.shift()!);
    }

    this.setStage(ServiceStage.initPlugins);
    this._extraPlugins.push(...this.initialPlugins);
    while (this._extraPlugins.length) {
      await this.initPlugin(this._extraPlugins.shift()!);
    }
  }

  getPluginAPI(opts: any) {
    const pluginAPI = new PluginAPI(opts);

    [
      'onPluginReady',
      'modifyPaths',
      'onStart',
      'modifyDefaultConfig',
      'modifyConfig',
    ].forEach((name) => {
      pluginAPI.registerMethod({ name, exitsError: false });
    });

    return new Proxy(pluginAPI, {
      get: (target, prop: string) => {
        // 由于 pluginMethods 需要在 register 阶段可用
        // 必须通过 proxy 的方式动态获取最新,以实现边注册边使用的效果
        if (this.pluginMethods[prop]) return this.pluginMethods[prop];
        if (
          [
            'applyPlugins',
            'ApplyPluginsType',
            'EnableBy',
            'ConfigChangeType',
            'babelRegister',
            'stage',
            'ServiceStage',
            'paths',
            'cwd',
            'pkg',
            'userConfig',
            'config',
            'env',
            'args',
            'hasPlugins',
            'hasPresets',
          ].includes(prop)
        ) {
          return typeof this[prop] === 'function'
            ? this[prop].bind(this)
            : this[prop];
        }
        return target[prop];
      },
    });
  }

  async applyAPI(opts: { apply: Function; api: PluginAPI }) {
    let ret = opts.apply()(opts.api);
    if (isPromise(ret)) {
      ret = await ret;
    }
    return ret || {};
  }

  // 初始化配置
  async initPreset(preset: IPreset) {
    const { id, key, apply } = preset;
    preset.isPreset = true;

    const api = this.getPluginAPI({ id, key, service: this });

    // register before apply
    this.registerPlugin(preset);
    // TODO: ...defaultConfigs 考虑要不要支持,可能这个需求可以通过其他渠道实现
    const { presets, plugins, ...defaultConfigs } = await this.applyAPI({
      api,
      apply,
    });

    // register extra presets and plugins
    if (presets) {
      assert(
        Array.isArray(presets),
        `presets returned from preset ${id} must be Array.`,
      );
      // 插到最前面,下个 while 循环优先执行
      this._extraPresets.splice(
        0,
        0,
        ...presets.map((path: string) => {
          return pathToObj({
            type: PluginType.preset,
            path,
            cwd: this.cwd,
          });
        }),
      );
    }

    // 深度优先
    const extraPresets = lodash.clone(this._extraPresets);
    this._extraPresets = [];
    while (extraPresets.length) {
      await this.initPreset(extraPresets.shift()!);
    }

    if (plugins) {
      assert(
        Array.isArray(plugins),
        `plugins returned from preset ${id} must be Array.`,
      );
      this._extraPlugins.push(
        ...plugins.map((path: string) => {
          return pathToObj({
            type: PluginType.plugin,
            path,
            cwd: this.cwd,
          });
        }),
      );
    }
  }

  // 初始化插件
  async initPlugin(plugin: IPlugin) {
    const { id, key, apply } = plugin;

    const api = this.getPluginAPI({ id, key, service: this });

    // register before apply
    this.registerPlugin(plugin);
    await this.applyAPI({ api, apply });
  }

  getPluginOptsWithKey(key: string) {
    return getUserConfigWithKey({
      key,
      userConfig: this.userConfig,
    });
  }

  // 注册插件
  registerPlugin(plugin: IPlugin) {
    // 考虑要不要去掉这里的校验逻辑
    // 理论上不会走到这里,因为在 describe 的时候已经做了冲突校验
    if (this.plugins[plugin.id]) {
      const name = plugin.isPreset ? 'preset' : 'plugin';
      throw new Error(`\
${name} ${plugin.id} is already registered by ${this.plugins[plugin.id].path}, \
${name} from ${plugin.path} register failed.`);
    }
    this.plugins[plugin.id] = plugin;
  }

  isPluginEnable(pluginId: string) {
    // api.skipPlugins() 的插件
    if (this.skipPluginIds.has(pluginId)) return false;

    const { key, enableBy } = this.plugins[pluginId];

    // 手动设置为 false
    if (this.userConfig[key] === false) return false;

    // 配置开启
    if (enableBy === this.EnableBy.config && !(key in this.userConfig)) {
      return false;
    }

    // 函数自定义开启
    if (typeof enableBy === 'function') {
      return enableBy();
    }

    // 注册开启
    return true;
  }

  // 判断函数:是否有插件
  hasPlugins(pluginIds: string[]) {
    return pluginIds.every((pluginId) => {
      const plugin = this.plugins[pluginId];
      return plugin && !plugin.isPreset && this.isPluginEnable(pluginId);
    });
  }

  // 判断函数:是否有预设
  hasPresets(presetIds: string[]) {
    return presetIds.every((presetId) => {
      const preset = this.plugins[presetId];
      return preset && preset.isPreset && this.isPluginEnable(presetId);
    });
  }

  // 真正的插件执行函数,基于promise实现
  async applyPlugins(opts: {
    key: string;
    type: ApplyPluginsType;
    initialValue?: any;
    args?: any;
  }) {
    const hooks = this.hooks[opts.key] || [];
    switch (opts.type) {
      case ApplyPluginsType.add:
        if ('initialValue' in opts) {
          assert(
            Array.isArray(opts.initialValue),
            `applyPlugins failed, opts.initialValue must be Array if opts.type is add.`,
          );
        }
        const tAdd = new AsyncSeriesWaterfallHook(['memo']);
        for (const hook of hooks) {
          if (!this.isPluginEnable(hook.pluginId!)) {
            continue;
          }
          tAdd.tapPromise(
            {
              name: hook.pluginId!,
              stage: hook.stage || 0,
              // @ts-ignore
              before: hook.before,
            },
            async (memo: any[]) => {
              const items = await hook.fn(opts.args);
              return memo.concat(items);
            },
          );
        }
        return await tAdd.promise(opts.initialValue || []);
      case ApplyPluginsType.modify:
        const tModify = new AsyncSeriesWaterfallHook(['memo']);
        for (const hook of hooks) {
          if (!this.isPluginEnable(hook.pluginId!)) {
            continue;
          }
          tModify.tapPromise(
            {
              name: hook.pluginId!,
              stage: hook.stage || 0,
              // @ts-ignore
              before: hook.before,
            },
            async (memo: any) => {
              return await hook.fn(memo, opts.args);
            },
          );
        }
        return await tModify.promise(opts.initialValue);
      case ApplyPluginsType.event:
        const tEvent = new AsyncSeriesWaterfallHook(['_']);
        for (const hook of hooks) {
          if (!this.isPluginEnable(hook.pluginId!)) {
            continue;
          }
          tEvent.tapPromise(
            {
              name: hook.pluginId!,
              stage: hook.stage || 0,
              // @ts-ignore
              before: hook.before,
            },
            async () => {
              await hook.fn(opts.args);
            },
          );
        }
        return await tEvent.promise();
      default:
        throw new Error(
          `applyPlugin failed, type is not defined or is not matched, got ${opts.type}.`,
        );
    }
  }

  // 运行方法
  async run({ name, args = {} }: { name: string; args?: any }) {
    args._ = args._ || [];
    if (args._[0] === name) args._.shift();

    this.args = args;
    await this.init();

    this.setStage(ServiceStage.run);
    await this.applyPlugins({
      key: 'onStart',
      type: ApplyPluginsType.event,
      args: {
        args,
      },
    });
    return this.runCommand({ name, args });
  }
  
  // 运行命令
  async runCommand({ name, args = {} }: { name: string; args?: any }) {
    assert(this.stage >= ServiceStage.init, `service is not initialized.`);

    args._ = args._ || [];
    if (args._[0] === name) args._.shift();

    const command =
      typeof this.commands[name] === 'string'
        ? this.commands[this.commands[name] as string]
        : this.commands[name];
    assert(command, `run command failed, command ${name} does not exists.`);

    const { fn } = command as ICommand;
    return fn({ args });
  }
}