UMI源码学习【2】——插件机制

22 分钟
UMI

UMI源码学习系列
UMI源码学习【1】——从umi dev到启动流程
UMI源码学习【2】——插件机制

插件初始化

插件的初始化操作在new Service().run()中执行 run中执行init

 async init() {
    // we should have the final hooksByPluginId which is added with api.register()
    this.initPresetsAndPlugins();

    // hooksByPluginId -> hooks
    // hooks is mapped with hook key, prepared for applyPlugins()
    // 把hooksByPluginId中内容映射到hooks中以key
    this.setStage(ServiceStage.initHooks);
    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);
      });
    });

    // plugin is totally ready
    this.setStage(ServiceStage.pluginReady);
    this.applyPlugins({
      key: 'onPluginReady',
      type: ApplyPluginsType.event,
    });
    ...省略与初始化插件无关的代码
  }

1、constructor中通过resolvePresets resolvePlugins 初始化initialPresets initialPlugins,会把每一个插件预计预设处理成固定格式,处理经过函数依次为resolvePlugins-->pathToObj,在pathToObj中会生成apply函数,通过apply函数可以拿到插件模块代码。

    this.initialPresets = resolvePresets({
      ...baseOpts,
      presets: opts.presets || [],
      userConfigPresets: this.userConfig.presets || [],
    });
    this.initialPlugins = resolvePlugins({
      ...baseOpts,
      plugins: opts.plugins || [],
      userConfigPlugins: this.userConfig.plugins || [],
    });
 
 // 每一个插件以及预设会处理成为下面的格式
  {
    id,
    key,
    path: winPath(path),
    apply() {
      // use function to delay require
      try {
        const ret = require(path);
        // use the default member for es modules
        return compatESModuleRequire(ret);
      } catch (e) {
        throw new Error(`Register ${type} ${path} failed, since ${e.message}`);
      }
    },
    defaultConfig: null,
  };

2、通过initPresetsAndPlugins初始化插件以及预设,在initPresetsAndPlugins中遍历插件以及预设,每一个插件以及预设都会通过getPluginAP方法获取到自己的api对象,调用apply函数注入参数api对象,在插件中可以拿到api中的方法处理一些逻辑。

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

    this.setStage(ServiceStage.initPlugins);
    this._extraPlugins.push(...this.initialPlugins);
    while (this._extraPlugins.length) {
      this.initPlugin(this._extraPlugins.shift()!);
    }
  }
 ----------------initPlugin------------------- 
  initPlugin(plugin: IPlugin) {
    const { id, key, apply } = plugin;

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

    // register before apply
    this.registerPlugin(plugin);
    apply()(api);
  }
 -----------------getPluginAPI---------------------
   getPluginAPI(opts: any) {
    const pluginAPI = new PluginAPI(opts);

    // register built-in methods
    // 给api注册自己的方法,即使重名依旧不报错
    [
      'onPluginReady',
      'modifyPaths',
      'onStart',
      'modifyDefaultConfig',
      'modifyConfig',
    ].forEach(name => {
      pluginAPI.registerMethod({ name, exitsError: false });
    });

    // 代理api的取值,加上service上的部分方法 nb
    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];
      },
    });
  }

3、然后把hooksByPluginId中内容格式映射一下到hooks中 hooksByPluginId是以插件id为维度对应各个函数如下

  hooksByPluginId: { // 插件id对应的hook函数数组
    [id: string]: IHook[];
    // {
    //   'pluginA': [{name:'插件名称',fn: ()=>{}},{name:'插件名称',fn: ()=>{}},{name:'插件名称',fn: ()=>{}}],
    //   'pluginB':  [{name:'插件名称',fn: ()=>{}},{name:'插件名称',fn: ()=>{}},{name:'插件名称',fn: ()=>{}}],
    // }
  } = {};

hooks是以插件调用函数名称为维度对应各个函数

  hooks: {
    [key: string]: IHook[]; // 插件调用函数名称:[每个插件调用的函数]
    // {
    //   'modifyRoutes': [{name:'插件名称',fn: ()=>{}},{name:'插件名称',fn: ()=>{}},{name:'插件名称',fn: ()=>{}}],
    //   'modifyHTML':  [{name:'插件名称',fn: ()=>{}},{name:'插件名称',fn: ()=>{}},{name:'插件名称',fn: ()=>{}}],
    // }
  } = {};
    // hooksByPluginId -> hooks
    // hooks is mapped with hook key, prepared for applyPlugins()
    // 把hooksByPluginId中内容映射到hooks中以key
    this.setStage(ServiceStage.initHooks);
    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);
      });
    });

插件API——new PluginAPI()

通过上面的分析可以发现绕不开的一个核心PluginAPI直接上PluginAPI源码一看究竟

export default class PluginAPI {
  id: string;
  key: string;
  service: Service;
  Html: typeof Html;
  utils: typeof utils;
  logger: Logger;

  constructor(opts: IOpts) {
  // 初始化参数省略,需要注意service,所有插件以及预设都是使用的同一个service
  }

  // 更新id key以及一些配置
  describe({
    id,
    key,
    config,
    enableBy,
  }: {
    id?: string;
    key?: string;
    config?: IPluginConfig;
    enableBy?: EnableBy | (() => boolean);
  } = {}) {
  // 省略源码可以去gitbub看看这里的操作
  }

  register(hook: IHook) {
    assert(
      hook.key && typeof hook.key === 'string',
      `api.register() failed, hook.key must supplied and should be string, but got ${hook.key}.`,
    );
    assert(
      hook.fn && typeof hook.fn === 'function',
      `api.register() failed, hook.fn must supplied and should be function, but got ${hook.fn}.`,
    );
    this.service.hooksByPluginId[this.id] = (
      this.service.hooksByPluginId[this.id] || []
    ).concat(hook);
  }

  // 注册命令到service上
  registerCommand(command: ICommand) {
    const { name, alias } = command;
    assert(
      !this.service.commands[name],
      `api.registerCommand() failed, the command ${name} is exists.`,
    );
    this.service.commands[name] = command;
    if (alias) {
      this.service.commands[alias] = name;
    }
  }

  // 注册预设
  registerPresets(presets: (IPreset | string)[]) {
    assert(
      this.service.stage === ServiceStage.initPresets,
      `api.registerPresets() failed, it should only used in presets.`,
    );
    assert(
      Array.isArray(presets),
      `api.registerPresets() failed, presets must be Array.`,
    );
    // 处理预设函数成一定格式 需要有apply函数apply可以获取到模块
    const extraPresets = presets.map(preset => {
      return isValidPlugin(preset as any)
        ? (preset as IPreset)
        : pathToObj({
            type: PluginType.preset,
            path: preset as string,
            cwd: this.service.cwd,
          });
    });
    // 插到最前面,下个 while 循环优先执行
    this.service._extraPresets.splice(0, 0, ...extraPresets);
  }

  // 在 preset 初始化阶段放后面,在插件注册阶段放前面
  registerPlugins(plugins: (IPlugin | string)[]) {
    assert(
      this.service.stage === ServiceStage.initPresets ||
        this.service.stage === ServiceStage.initPlugins,
      `api.registerPlugins() failed, it should only be used in registering stage.`,
    );
    assert(
      Array.isArray(plugins),
      `api.registerPlugins() failed, plugins must be Array.`,
    );
    const extraPlugins = plugins.map(plugin => {
      return isValidPlugin(plugin as any)
        ? (plugin as IPreset)
        : pathToObj({
            type: PluginType.plugin,
            path: plugin as string,
            cwd: this.service.cwd,
          });
    });
    if (this.service.stage === ServiceStage.initPresets) {
      this.service._extraPlugins.push(...extraPlugins);
    } else {
      this.service._extraPlugins.splice(0, 0, ...extraPlugins);
    }
  }

  // 注册方法,方法是注册到了service中的pluginMethods
  registerMethod({
    name,
    fn,
    exitsError = true,
  }: {
    name: string;
    fn?: Function;
    exitsError?: boolean;
  }) {
    // 不同插件也不能注册同名方法
    if (this.service.pluginMethods[name]) {
      if (exitsError) {
        throw new Error(
          `api.registerMethod() failed, method ${name} is already exist.`,
        );
      } else {
        return;
      }
    }
    // 如果有fn注册fn,没有fn则注册一个默认函数,执行这个函数会注册一个hook到 this.service.hooksByPluginId
    // 比如三个插件使用了内置api modifyRoutes 修改路由
    // new了三个PluginAPI(); 由于在preset-built-in/registerMethods中注册好了扩展方法,当api.modifyRoutes执行的时候会走到getPluginApi中的new Proxy代理,拿到service对象上的pluginMethod,也就这个默认函数。
    // 这个默认函数执行的时候可以注册hook到 this.service.hooksByPluginId,  然后service中把hooksByPluginId转到hooks中
    // 然后在routers中通过applyPlugins执行这个函数以及每个插件的hook累计结果输出
    this.service.pluginMethods[name] =
      fn ||
      // 这里不能用 arrow function,this 需指向执行此方法的 PluginAPI
      // 否则 pluginId 会不会,导致不能正确 skip plugin
      function(fn: Function) {
        const hook = {
          key: name,
          ...(utils.lodash.isPlainObject(fn) ? fn : { fn }),
        };
        // @ts-ignore
        this.register(hook);
      };
  }

  // 跳过插件
  skipPlugins(pluginIds: string[]) {
    pluginIds.forEach(pluginId => {
      this.service.skipPluginIds.add(pluginId);
    });
  }
}

可以看到PluginAPI负责生成API对象传参给每一个插件,每一个api共享同一个service对象,然后代理一下API对象,可以获取到service中的部分方法,核心方法就是PluginAPI中含有的方法,然后在在preset-built-in/registerMethods中注册好了扩展方法,所以写UMI插件的时候一些内置方法提供我们使用。当访问api.modifyRoutes的时候,会走到这里getPluginAPI中的这行代码,返回service对象中的pluginMethods[prop](也就是在preset-built-in/registerMethods中注册的扩展方法,每个扩展方法的执行都会添加hook到对应的this.service.hooksByPluginId中)

if (this.pluginMethods[prop]) return this.pluginMethods[prop];

插件的触发执行——applyPlugins

那疑问来了,比如我写了三个插件来修改路由,如下,那么是如何触发修改的???

------------插件1----------------
const func1 = (memo) => {
  Object.keys(memo).forEach((id) => {
    const route = memo[id];
    if(route.path === '/test1'){
      route.path = '/redirect1'
    }
  });
  return memo;
}
api.modifyRoutes(func1)
------------插件2----------------
const func2 = (memo) => {
  Object.keys(memo).forEach((id) => {
    const route = memo[id];
    if(route.path === '/test2'){
      route.path = '/redirect2'
    }
  });
  return memo;
}
api.modifyRoutes(func2)
------------插件3----------------
cont fun3 = (memo) => {
  Object.keys(memo).forEach((id) => {
    const route = memo[id];
    if(route.path === '/test3'){
      route.path = '/redirect3'
    }
  });
  return memo;
}
api.modifyRoutes(fun3)

经过上面的分析,首先这三个插件的api.modifyRoutes的执行都会把func1、func2、func3添加到hooksByPluginId中结构如下

hooksByPluginId = {
 'plugin1':[{name: 'modifyRoutes', fn: func1}],
 'plugin2':[{name: 'modifyRoutes', fn: func2}],
 'plugin3':[{name: 'modifyRoutes', fn: func3}]
}

然后在service中会把hooksByPluginId转到hooks中结构如下:

hooks = {
  'modifyRoutes': [
      {pluginId: 'plugin1', key: 'modifyRoutes', fn: fun1},
      {pluginId: 'plugin2', key: 'modifyRoutes', fn: fun2},
      {pluginId: 'plugin3', key: 'modifyRoutes', fn: fun3},
  ]
}

到此还没有触发modifyRoutes,触发是在源码中prereset-built-in中注册的路由插件中执行的源码如下:

import { IApi } from '@umijs/types';
import { Route } from '@umijs/core';

export default function(api: IApi) {
  api.describe({
    key: 'routes',
    config: {
      schema(joi) {
        return joi.array().items(joi.object());
      },
      onChange: api.ConfigChangeType.regenerateTmpFiles,
    },
  });

  api.registerMethod({
    name: 'getRoutes',
    async fn() {
      const route = new Route({
        async onPatchRoutes(args: object) {
          await api.applyPlugins({
            key: 'onPatchRoutes',
            type: api.ApplyPluginsType.event,
            args,
          });
        },
        async onPatchRoute(args: object) {
          await api.applyPlugins({
            key: 'onPatchRoute',
            type: api.ApplyPluginsType.event,
            args,
          });
        },
      });
      return await api.applyPlugins({
        key: 'modifyRoutes',
        type: api.ApplyPluginsType.modify,
        initialValue: await route.getRoutes({
          config: api.config,
          root: api.paths.absPagesPath!,
        }),
      });
    },
  });
}

其中api.applyPlugins用于触发插件的执行,api.applyPlugins函数从哪里来呐?看到这里我直呼nb,调用api.applyPlugins的时候会走到getPluginAPI设置的代理,获取到service对象上的applyPlugins函数,看一下applyPlugins函数负责的逻辑是什么,来上源码

import { AsyncSeriesWaterfallHook } from 'tapable';

  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}.`,
        );
    }
  }

建议先了解一下tapable这个包是干嘛用的,简单来讲可以异步串行执行结果,把上一次结果丢给下一次执行,类似js中reduce。
分类型执行插件,不同类型处理方式不一样。
api.modifyRoutes中的type是api.ApplyPluginsType.modify,所以在applyPlugins中看到取出modifyRoutes对应的hook数组,然后给根据初始值依次执行hooks.fn函数

const hooks = this.hooks[opts.key] || [];

总结

到此UMI的核心插件系统大致熟悉了,其中有许多细节没有提到,反复阅读源码可以学到很多东西。UMI剩余的比如路由、react渲染、dva等都是以插件形式进行注册的。

umi如何渲染的?
umi如何与react想结合的?
umi的路由系统怎么生成的?


此文自动发布于:github issues