umi 探究

umi 如何断点调试

clone 源代码到本地,修改.fatherrc.base.ts, 添加sourcemap: true,这样就可以愉快的debugger了;

umi 如何启动

umi dev

dev 也是一个插件;本质是实例化一个Server, 然后执行run方法,最后执行dev插件的command;(placeholder, 有时间后拓展)

umi 插件机制

核心:

packages/umi/src/service/service.ts

Service 继承自 CoreService(packages/core/src/service/service.ts);

实例化(引入内部预设插件集)

run(注册内部预设插件集, 依次触发modifyPaths,modifyTelemetryStorage,modifyAppData,onCheck,onStart, 执行dev命令)

那么触发的这些事件后续触发了什么操作呢?涉及到如下:

umi 插件如何订阅事件

例如onCheck,可以看到有很多预设插件有订阅该事件:

查看任意一个代码如下:

csharp 复制代码
api.onCheck(() => {
    // mako 仅支持 node 16+
    // ref: https://github.com/umijs/mako/issues/300
    checkVersion(16, `Node 16 is required when using mako.`);
});

可以看到是一个版本检查的逻辑;

那么这个api是如何注入的呢?

umi 插件如何注册与注入框架能力
  1. 通过Plugin.getPluginsAndPresets 实例化插件集为Plugin实例数组,过程中会在外层包裹apply
  2. 在service 的实例方法initPlugin中 实例化 PluginAPI 生成 pluginAPI, 并为pluginAPI 包裹一层代理,使其能够访问Service实例的部分方法与属性:
service/pluginAPI.ts 复制代码
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];
    },
  });
}
  1. 将plugin API 注入 plugin;
ini 复制代码
let ret = await opts.plugin.apply()(proxyPluginAPI);

可以看到PluginAPI 的实例和proxy 里 Service的实例并没有 onCheck, 那么它是哪来的呢?

其实是在servicePlugin 中注册的,它在所有内置插件实例化之前先实例化:

service/servicePlugin.ts 复制代码
export default (api: PluginAPI) => {
  [
    'onCheck',
    'onStart',
    'modifyAppData',
    'modifyConfig',
    'modifyDefaultConfig',
    'modifyPaths',
    'modifyTelemetryStorage',
  ].forEach((name) => {
    api.registerMethod({ name }); // PluginAPI 实例提供 registerMethod
  });
};
typescript 复制代码
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 }),
        });
      },
  };
}

这样其他后面的插件就能使用api.onCheck来注册监听事件了;这种方式onChenck (也就是被代理出的this.service.pluginMethods.onCheck.fn) 底层是调用了内置key的regsiter 方法:

umi 如何触发事件

使用api.applyPlugins 触发事件:底层使用tapable来处理多个事件的触发顺序;

截取部分代码:(触发类型为event, 异步执行):

javascript 复制代码
const tEvent = new AsyncSeriesWaterfallHook(['_']); // 创建一个异步串行流水钩子;虽是流水钩子,但是没有用到操作结果执行函数传递; 可以查看modify的处理,有串行参数,也就是第二个参数的入参
for (const hook of hooks) {
  if (!this.isPluginEnable(hook)) continue;
  tEvent.tapPromise(
    {
      name: hook.plugin.key,
      stage: hook.stage || 0,
      before: hook.before,
    },
    async () => {
      const dateStart = new Date();
      await hook.fn(opts.args);
      hook.plugin.time.hooks[opts.key] ||= [];
      hook.plugin.time.hooks[opts.key].push(
        new Date().getTime() - dateStart.getTime(),
      );
    },
  );
}
return tEvent.promise(1) as Promise<T>; // 开始执行,初始参数为1;
客户端插件机制

umi.ts 入口文件(默认没有修改的情况)的生成(如下代码揭示了入口文件的路径;初始值为umi.ts):

php 复制代码
const entry = await api.applyPlugins({
    key: 'modifyEntry',
    initialValue: {
      umi: join(api.paths.absTmpPath, 'umi.ts'),
    },
})

packages/preset-umi/src/commands/dev/dev.ts 下发事件('onGenerateFiles')

packages/preset-umi/src/features/tmpFiles/tmpFiles.ts; 注册事件('onGenerateFiles'),触发时会调用多个applyPlugins来获取模板数据,并渲染模板(umi.tpl);

umi.ts 执行渲染的代码:

.umi/umi.ts 复制代码
const render = () => {
    // ....
    return (pluginManager.applyPlugins({
        key: 'render',
        type: ApplyPluginsType.compose,
        initialValue() {
          const context = {
            routes,
            routeComponents,
            pluginManager,
            rootElement: contextOpts.rootElement || document.getElementById('root'),
            publicPath,
            runtimePublicPath,
            history,
            historyType,
            basename,
            callback: contextOpts.callback,
          };
          const modifiedContext = pluginManager.applyPlugins({
            key: 'modifyClientRenderOpts',
            type: ApplyPluginsType.modify,
            initialValue: context,
          });
          return renderClient(modifiedContext);
        },
    }))()
}
render();

renderClient 里面有对应内置插件的调用,构成根元素的子组件:

renderer-react/src/browser.tsx 复制代码
// 加载所有需要的插件
for (const key of UMI_CLIENT_RENDER_REACT_PLUGIN_LIST) {
    rootContainer = opts.pluginManager.applyPlugins({
      type: 'modify',
      key: key,
      initialValue: rootContainer,
      args: {
        routes: opts.routes,
        history: opts.history,
        plugin: opts.pluginManager,
      },
    });
}

modify相关的处理逻辑:

typescript 复制代码
 return hooks.reduce((memo: any, hook: Function | object) => {
    assert(
      typeof hook === 'function' || typeof hook === 'object',
      `applyPlugins failed, all hooks for key ${key} must be function or plain object.`,
    );
    if (typeof hook === 'function') {
      return hook(memo, args);
    } else {
      // TODO: deepmerge?
      return { ...memo, ...hook };
    }
  }, initialValue);

内置插件:innerProvider -> i18nProvider -> accessProvider -> dataflowProvider -> outerProvider -> rootContainer;

嵌套顺序相反,最后一个在最外层;

其他

约定式路由如何生成:

获取routes在packages/preset-umi/src/features/appData/appData.ts 插件中;

------------------------------------------------------ 分割新 ------------------------------

后续待更;

参考资料

UMI3源码解析系列之插件化架构核心

相关推荐
影子落人间1 分钟前
已解决npm ERR! request to https://registry.npm.taobao.org/@vant%2farea-data failed
前端·npm·node.js
世俗ˊ26 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92126 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_31 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人40 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
MinIO官方账号1 小时前
从 HDFS 迁移到 MinIO 企业对象存储
人工智能·分布式·postgresql·架构·开源
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css