找回密码
 会员注册
查看: 22|回复: 0

UMI3源码解析系列之运行时插件机制

[复制链接]

1

主题

0

回帖

4

积分

新手上路

积分
4
发表于 2024-9-19 18:01:15 | 显示全部楼层 |阅读模式
前言前面几篇文章,我们分析了umijs「核心」Service「类的初始化流程」、「插件化的核心流程」以及「构建阶段」dev「命令的执行流程」。那我们今天继续分析项目在「运行时阶段」又会做哪些事呢?在开始今天的文章之前,大家不妨想几个问题:「自动生成的入口文件」和我们平时自己写的入口文件有什么不一样?项目中使用的插件(例如:plugin-model、plugin-request等插件)又是「如何注册到项目」中的?在运行时阶段,如何「动态修改路由」或者如何「重写」render「方法」?umijs生成的「临时文件通过其他构建工具」(不限于webpack、rollup、esbuild)可以跑起来吗?入口文件在前面我们提到了在解析preset-built-in预设阶段会批量导入generatefiles相关的plugin,同时在这些plugin中注册onGenerateFiles钩子,然后在webpack编译前触发,生成临时文件。接下来,我们从入口文件出发,看下通过执行umidev命令生成的入口文件内容:// path: src/.umi/umi.ts// @ts-nocheckimport './core/polyfill';import '@@/core/devScripts';import { plugin } from './core/plugin';import './core/pluginRegister';import { createHistory } from './core/history';import { ApplyPluginsType } from '~/umi-test/node_modules/@umijs/runtime';import { renderClient } from '~/umi-test/node_modules/@umijs/renderer-react';import { getRoutes } from './core/routes';const getClientRender = (args: { hot?: boolean; routes?: any[] } = {}) => plugin.applyPlugins({  key: 'render',  type: ApplyPluginsType.compose,  initialValue: () => {    const opts = plugin.applyPlugins({      key: 'modifyClientRenderOpts',      type: ApplyPluginsType.modify,      initialValue: {        routes: args.routes || getRoutes(),        plugin,        history: createHistory(args.hot),        isServer: process.env.__IS_SERVER,        rootElement: 'root',        defaultTitle: ``,      },    });    return renderClient(opts);  },  args,});const clientRender = getClientRender();export default clientRender();window.g_umi = {  version: '3.5.14',};// hot module replacement...首先可以看到文件的顶部导入了polyfill文件,也就是我们平时自己开发项目导入的babel相关的polyfill文件。❝当然在umi-next版本已经在尝试用swc代替babel,感兴趣的小伙伴可以自行查阅umi-next的相关issues。❞// path: src/.umi/umi.ts// @ts-nocheckimport 'core-js';import 'regenerator-runtime/runtime';export {};接下来我们继续往下分析,通过执行getClientRender函数,返回clientRender。在getClientRender函数内部我们看到了熟悉的面孔--plugin,运行时阶段同样通过插件化返回渲染需要的render方法。值得注意的是这里的Plugin是有别于前面提到的PluginAPI,PluginAPI是在作用于编译阶段,而Plugin是作用于运行时的插件。接下来,我们看下运行时插件是怎么实现的。运行时插件❝阅读源码最好的出入点就是从它的测试用例出发。测试用例是题干,源码就是答案。------加夫列尔·加西亚·马尔波斯❞「接下来我们从不同方向出发,更好的了解运行时插件机制的原理及实现。」从Plugin的测试用例出发接下来,我们看下几个plugin的「测试用例」:实例化时设置可允许注册的key,同时在register时会校验key是否允许test('invalid key', () => {  const p = new lugin({    validKeys: [],  });  expect(() => {    p.register({      apply: { foo: 1 },      path: '/foo.js',    });  }).toThrow(/invalid key foo from plugin \/foo.js/);});通过getHooks方法可获取指定key注册的hooktest('getHooks', () => {  const p = new lugin({    validKeys: ['foo'],  });  p.register({    apply: { foo: 1 },    path: '/foo1.js',  });  p.register({    apply: { foo: 2 },    path: '/foo2.js',  });  expect(p.getHooks('foo')).toEqual([1, 2]);});通过applyPlugins方法可执行指定key注册的hook,同时还支持以下三种操作:支持依次把上一个hook的返回值作为入参传递给下一个hook支持args当前hook的其他参数支持默认值initialValue// path: ~/umi/packages/runtime/src/Plugin/Plugin.test.tstest('applyPlugins modify', () => {  const p = new lugin({    validKeys: ['foo'],  });  p.register({    apply: {      foo(memo: object) {        return { ...memo, a: 1 };      },    },    path: '/foo1.js',  });  p.register({    apply: {      foo(memo: object, args: { step: number }) {        return { ...memo, b: 1 + ((args & args.step) || 0) };      },    },    path: '/foo2.js',  });  p.register({    apply: {      foo: {        a: 2,        c: 1,      },    },    path: '/foo3.js',  });  // 1. 把上一个hook的返回值作为入参传递给下一个hook  expect(    p.applyPlugins({      key: 'foo',      type: ApplyPluginsType.modify,    }),  ).toEqual({    a: 2,    b: 1,    c: 1,  });  // 2. 支持args当前hook的其他参数  expect(    p.applyPlugins({      key: 'foo',      type: ApplyPluginsType.modify,      args: { step: 5 },    }),  ).toEqual({    a: 2,    b: 6,    c: 1,  });  // 3. 支持默认值initialValue  expect(    p.applyPlugins({      key: 'foo',      type: ApplyPluginsType.modify,      initialValue: { d: 4 },    }),  ).toEqual({    a: 2,    b: 1,    c: 1,    d: 4,  });});通过分析Plugin常见的「测试用例」,我们大致知道了Plugin的使用方法,那么接下来我们从「源码」出发,更加进一步的了解Plugin的工作流程。从Plugin源码出发// path: ~/umi/packages/runtime/src/Plugin/Plugin.tsexport default class lugin {  validKeys: string[];  hooks: {    [key: string]: any;  } = {};  constructor(opts?: IOpts) {    this.validKeys = opts?.validKeys || [];  }  // 注册插件  register(plugin: IPlugin) {    assert(!!plugin.apply, `register failed, plugin.apply must supplied`);    assert(!!plugin.path, `register failed, plugin.path must supplied`);    Object.keys(plugin.apply).forEach((key) => {      assert(        this.validKeys.indexOf(key) > -1,        `register failed, invalid key ${key} from plugin ${plugin.path}.`,      );      if (!this.hooks[key]) this.hooks[key] = [];      this.hooks[key] = this.hooks[key].concat(plugin.apply[key]);    });  }    // 获取 hook  getHooks(keyWithDot: string) {    const [key, ...memberKeys] = keyWithDot.split('.');    let hooks = this.hooks[key] || [];    if (memberKeys.length) {      hooks = hooks        .map((hook: any) => {          try {            let ret = hook;            for (const memberKey of memberKeys) {              ret = ret[memberKey];            }            return ret;          } catch (e) {            return null;          }        })        .filter(Boolean);    }    return hooks;  }  // 执行 hook  applyPlugins({    key,    type,    initialValue,    args,    async,  }) {    const hooks = this.getHooks(key) || [];    switch (type) {      case ApplyPluginsType.modify:        if (async) {          return hooks.reduce(            async (memo: any, hook: Function | romise
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 11:32 , Processed in 1.673816 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表