umi源码-05注册(初始化)插件和插件集

212 阅读5分钟

插件集是插件的集合,本质还是插件。因此,初始化插件集其实就是遍历插件集中的插件,然后初始化。初始化插件也是遍历所有插件,然后初始化。

初始化插件其实就是执行 new PluginAPI()

PluginAPI 的原理

Umi 会为每个插件赋予一个 PluginAPI 对象,这个对象引用了插件本身和 Umi 的 service。 Umi 为 PluginAPI 对象的 get() 方法进行了 proxy, 具体规则如下:

  • pluginMethod: 如果 prop 是 Umi 所维护的 pluginMethods[] ( 通过 registerMethod() 注册的方法 )中的方法,则返回这个方法。
  • service props: 如果 prop 是 serviceProps 数组中的属性(这些属性是 Umi 允许插件直接访问的属性),则返回 service 对应的属性。
  • static props: 如果 prop 是参数 staticProps 数组中的属性(这些属性是静态变量,诸如一些类型定义和常量),则将其返回。
  • 否则返回 api 的属性

因此,Umi 提供给插件的 api 绝大多数都是依靠 registerMethod() 来实现的,你可以直接使用我们的这些 api 快速地在插件中注册 hook。这也是 Umi 将框架和功能进行解耦的体现: Umi 的 service 只提供插件的管理功能,而 api 都依靠插件来提供。

注册插件可以获得的能力

new PluginAPI 获得的能力, 参考 umi 核心 API文档

  • describe 描述插件
  • registerCommand 注册命令
  • registerGenerator
  • register 为 api.applyPlugins 注册可供其使用的 hook
  • registerMethod 往 api 上注册一个名为 'name' 的方法
  • registerPresets 注册插件集
  • registerPlugins 注册插件
  • skipPlugins 声明哪些插件需要被禁用

service中获取的能力

  • service获取的能力都放在了 pluginMethods,而pluginMethods 通过 registerMethod设置。通过下面代码一目了然。因此这里是用户可扩展的能力。实际上umi内部的扩展方法都是通过 registerMethod实现的。具体参考 plugin-api#扩展方法。下一篇我们会继续讲这个方法。

registerMethod(opts: { name: string; fn?: Function }) {
  assert(
    !this.service.pluginMethods[opts.name],
    `api.registerMethod() failed, method ${opts.name} is already exist.`,
  );
  this.service.pluginMethods[opts.name] = {
    plugin: this.plugin,
    fn:
      opts.fn ||
      // 这里不能用 arrow function,this 需指向执行此方法的 PluginAPI
      // 否则 pluginId 会不会,导致不能正确 skip plugin
      function (fn: Function | Object) {
        // @ts-ignore
        this.register({
          key: opts.name,
          ...(lodash.isPlainObject(fn) ? (fn as any) : { fn }),
        });
      },
  };
}


register(opts: Omit<IHookOpts, 'plugin'>) {
  assert(
    this.service.stage <= ServiceStage.initPlugins,
         'api.register() should not be called after plugin register stage.',
        );
this.service.hooks[opts.key] ||= [];
this.service.hooks[opts.key].push(
  new Hook({ ...opts, plugin: this.plugin }),
);
}

export class Hook {
  plugin: Plugin;
  key: string;
  fn: Function;
  before?: string;
  stage?: number;
  constructor(opts: IOpts) {
    assert(
      opts.key && opts.fn,
      `Invalid hook ${opts}, key and fn must supplied.`,
    );
    this.plugin = opts.plugin;
    this.key = opts.key;
    this.fn = opts.fn;
    this.before = opts.before;
    this.stage = opts.stage || 0;
  }
}

serviceProps获取的能力

都是一些变量,这些变量都在 service中设置过,可以直接用。因此sercice中的这些暴漏出来的变量也是所有插件通用的。我们可以基于这些变量做一些事情。

  • appData
  • applyPlugins
  • args
  • config
  • cwd
  • pkg
  • pkgPath
  • name
  • paths
  • userConfig
  • env
  • isPluginEnable

staticProps获取的能力

  • ApplyPluginsType 包含 add、modify、event
  • ConfigChangeType 包含 reload、regenerateTmpFiles
  • EnableBy 包含 register、config
  • ServiceStage 生命周期 包含uninitialized、init、initPresets、initPlugins、resolveConfig、collectAppData、onCheck、onStart、runCommand
  • service 包含的就比较多了,基本就是 所以核心能力了,其中比较重要的事 applyPlugins
// 第一步:实例化插件
const pluginAPI = new PluginAPI({
  plugin: opts.plugin,
  service: this,
});
// 第二步:给插件添加 registerPresets方法
pluginAPI.registerPresets = pluginAPI.registerPresets.bind(
  pluginAPI,
  opts.presets || [],
);
// 第三步:给插件添加 registerPlugins 方法
pluginAPI.registerPlugins = pluginAPI.registerPlugins.bind(
  pluginAPI,
  opts.plugins,
);
// 插件熟悉劫持,其中 包含了前面三部之后的 pluginAPI 所以方法。
const proxyPluginAPI = PluginAPI.proxyPluginAPI({
  service: this,
  pluginAPI,
  serviceProps: [
    'appData',
    'applyPlugins',
    'args',
    'config',
    'cwd',
    'pkg',
    'pkgPath',
    'name',
    'paths',
    'userConfig',
    'env',
    'isPluginEnable',
  ],
  staticProps: {
    ApplyPluginsType,
    ConfigChangeType,
    EnableBy,
    ServiceStage,
    service: this,
  },
});

// 这里的 proxyPluginAPI 其实就是插件的入参实参,相当于给插件挂载了很多的方法供用户调用
let ret = await opts.plugin.apply()(proxyPluginAPI);

我们来看一个简单的插件, 插件中的 api 等价于 上面所说的 proxyPluginAPI。

import { IApi } from 'umi';

export default (api: IApi) => {
  api.describe({
    key: 'changeFavicon',
    config: {
      schema(joi) {
        return joi.string();
      },
    },
    enableBy: api.EnableBy.config
  });
  api.modifyConfig((memo)=>{
    memo.favicon = api.userConfig.changeFavicon;
    return memo;
  });
};

下面在贴一下 proxy的写法,知道proxy的应该都明白,这里简单看一下

  • 首先去pluginMethods找对应的方法或者值,找到了就返回
  • 然后去 serviceProps 找对应的方法或者值,找到了就返回
  • 再然后去 staticProps 找对应的方法或者值,找到了就返回
  • 最后才是target(pluginAPI)找对应的方法或者值,找到了就返回
static proxyPluginAPI(opts: {
                      pluginAPI: PluginAPI;
service: Service;
serviceProps: string[];
staticProps: Record<string, any>;
}) {
  return new Proxy(opts.pluginAPI, {
    get: (target, prop: string) => {
      if (opts.service.pluginMethods[prop]) {
        return opts.service.pluginMethods[prop].fn;
      }
      if (opts.serviceProps.includes(prop)) {
        // @ts-ignore
        const serviceProp = opts.service[prop];
        return typeof serviceProp === 'function'
          ? serviceProp.bind(opts.service)
          : serviceProp;
      }
      if (prop in opts.staticProps) {
        return opts.staticProps[prop];
      }
      // @ts-ignore
      return target[prop];
    },
  });
}

插件中可以嵌套插件

插件可以返回插件或者插件集,看一个返回插件和插件集的例子

import { IApi } from 'umi';
 
export default (api: IApi) => {
  return {
    plugins: ['./plugin_foo','./plugin_bar'],
    presets: ['./preset_foo']
  }
};

针对这种情况umi怎么处理?请看下面的代码。 其实也很简单,遍历插件或者插件集,通过 new Plugin 实例化

if (ret?.presets) {
      ret.presets = ret.presets.map(
        (preset: string) =>
          new Plugin({
            path: preset,
            type: PluginType.preset,
            cwd: this.cwd,
          }),
      );
    }
    if (ret?.plugins) {
      ret.plugins = ret.plugins.map(
        (plugin: string) =>
          new Plugin({
            path: plugin,
            type: PluginType.plugin,
            cwd: this.cwd,
          }),
      );
    }

插件的描述信息

每个插件应当有描述信息,在插件注册阶段( initPresets or initPlugins stage )执行,用于描述插件或者插件集的 key、配置信息和启用方式等。当然,如果你没有写描述信息,也不会报错。 一个描述信息大概这样

api.describe({
  key: 'foo',
  config: {
    default: 'Hello, Umi!',
    schema(joi){
      return joi.string();
    },
    onChange: api.ConfigCHangeType.regenerateTmpFiles,
  },
  enableBy: api.EnableBy.config,
})

这个 describe方法其实就是在插件注册的时候执行的,上面我们已经提过过了,通过 new PluginAPI获得了 describe 的能力。这个方法其实也很简单,就是把key、config等参数放入实例化的插件中

describe(opts: {
  key?: string;
  config?: IPluginConfig;
  enableBy?:
  | EnableBy
  | ((enableByOpts: { userConfig: any; env: Env }) => boolean);
}) {
  // default 值 + 配置开启冲突,会导致就算用户没有配 key,插件也会生效
  if (opts.enableBy === EnableBy.config && opts.config?.default) {
    throw new Error(
      `[plugin: ${this.plugin.id}] The config.default is not allowed when enableBy is EnableBy.config.`,
    );
  }
  this.plugin.merge(opts);
}

那么这个描述信息有什么用呢?下面我们看插件注册阶段对描述信息的处理

// collect configSchemas and configDefaults
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;
}

可以看到,这些信息最后被放在 service的三个对象里面,分别是 this.configSchemas[key], this.configDefaults[key], this.configOnChanges[key],后面会用到。

到这里初始化阶段已经基本完成,下一篇我们看 resolve 阶段。