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源码解析系列之插件化架构核心

相关推荐
Jiaberrr几秒前
Vite环境下uniapp Vue 3项目添加和使用环境变量的完整指南
前端·javascript·vue.js·uni-app
Marry1.09 分钟前
uniapp背景图用本地图片
前端·uni-app
夏河始溢15 分钟前
一七八、Node.js PM2使用介绍
前端·javascript·node.js·pm2
记忆深处的声音15 分钟前
vue2 + Element-ui 二次封装 Table 组件,打造通用业务表格
前端·vue.js·代码规范
陈随易16 分钟前
兔小巢收费引发的论坛调研Node和Deno有感
前端·后端·程序员
熊的猫31 分钟前
webpack 核心模块 — loader & plugins
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
速盾cdn37 分钟前
速盾:vue的cdn是干嘛的?
服务器·前端·网络
四喜花露水1 小时前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy1 小时前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
洛卡卡了2 小时前
从单层到 MVC,再到 DDD:架构演进的思考与实践
架构·mvc