umi源码阅读3:渲染过程

819 阅读5分钟

umi阅读分为5个部分,分别是:

以下代码以umi的3.5.20版本为例,主要内容以源码+个人解读为主

上一篇:umi源码阅读2:实例化过程

渲染过程

已知umi主要用于react的开发,会根据项目的routes来组织项目的路由。接下来内容是研究umi如何跟react挂上钩的,以及使用routes的渲染的入口。

上一个过程讲到了umi默认使用webpack编译,而webpack编译的需要进行基本的配置入口文件entry,那umi的入口文件在哪呢?

找到packages/preset-built-in/plugins/buildDevUtils.ts中的getBundleAndConfigs方法,在该方法中调用插件modifyBundleConfigOpts定义了entry

entry: {
   umi: join(api.paths.absTmpPath!, 'umi.ts'),
},

这里采用的是多入口模式,chunk的名称就是umi.js,而路由指向的是umi的临时文件。umi项目在启动之后,会提前生成所需的临时文件、模板代码等,包括路由,代理,插件等临时文件,显然这里指向了umi生成的临时文件。

[umi项目临时文件路径]/umi.ts

// @ts-nocheck
import './core/polyfill';
import '@@/core/devScripts';
import '[项目路径]/node_modules/intl/index.js';
import { plugin } from './core/plugin';
import './core/pluginRegister';
import { createHistory } from './core/history';
import { ApplyPluginsType } from '[项目路径]/node_modules/umi/node_modules/@umijs/runtime';
import { renderClient } from '[项目路径]/node_modules/@umijs/renderer-react/dist/index.js';
import { getRoutes } from './core/routes';

import { _onCreate } from './plugin-locale/locale';
_onCreate();

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.36',
    };
  

// hot module replacement
// @ts-ignore
if (module.hot) {
  // @ts-ignore
  module.hot.accept('./core/routes', () => {
    const ret = require('./core/routes');
    if (ret.then) {
      ret.then(({ getRoutes }) => {
        getClientRender({ hot: true, routes: getRoutes() })();
      });
    } else {
      getClientRender({ hot: true, routes: ret.getRoutes() })();
    }
  });
}

render插件中,注入了渲染相关的参数,包括routes,history,rootElement等,然后执行了clientRender,而插件中关键点是renderClient,根据地址找到renderClient函数

export default function renderClient(opts: IOpts) {
  const rootContainer = opts.plugin.applyPlugins({
    type: ApplyPluginsType.modify,
    key: 'rootContainer',
    initialValue: (
      <RouterComponent
        history={opts.history}
        routes={opts.routes}
        plugin={opts.plugin}
        ssrProps={opts.ssrProps}
        defaultTitle={opts.defaultTitle}
      />
    ),
    args: {
      history: opts.history,
      routes: opts.routes,
      plugin: opts.plugin,
    },
  });

	// 渲染到根节点 
  if (opts.rootElement) {
    const rootElement =
      typeof opts.rootElement === 'string'
        ? document.getElementById(opts.rootElement)
        : opts.rootElement;
    const callback = opts.callback || (() => {});

    if (window.g_useSSR) {
      if (opts.dynamicImport) {
        preloadComponent(opts.routes).then(function () {
          hydrate(rootContainer, rootElement, callback);
        });
      } else {
        hydrate(rootContainer, rootElement, callback);
      }
    } else {
      render(rootContainer, rootElement, callback);
    }
  } else {
    return rootContainer;
  }
}

在这里,使用react渲染页面,同时包裹了了RouterComponent组件。RouterComponent提供了路由变更监听的工作。

function RouterComponent(props: IRouterComponentProps) {
  const { history, ...renderRoutesProps } = props;

  useEffect(() => {
    
    // 路由变更监听操作
    function routeChangeHandler(location: any, action?: string) {
      const matchedRoutes = matchRoutes(
        props.routes as RouteConfig[],
        location.pathname,
      );

      // 设置document标题
      if (
        typeof document !== 'undefined' &&
        renderRoutesProps.defaultTitle !== undefined
      ) {
        document.title =
          (matchedRoutes.length &&
            // @ts-ignore
            matchedRoutes[matchedRoutes.length - 1].route.title) ||
          renderRoutesProps.defaultTitle ||
          '';
      }
      props.plugin.applyPlugins({
        key: 'onRouteChange',
        type: ApplyPluginsType.event,
        args: {
          routes: props.routes,
          matchedRoutes,
          location,
          action,
        },
      });
    }
    routeChangeHandler(history.location, 'POP');
    return history.listen(routeChangeHandler);
  }, [history]);

  return <Router history={history}>{renderRoutes(renderRoutesProps)}</Router>;
}

rootContainer又是什么呢?

看代码rootContainer是插件,而这个插件实例从[umi项目临时文件路径]/umi.ts看同样来自临时文件[umi项目临时文件路径]/core/plugin.ts,我们继续看。

import { Plugin } from '[项目路径]/node_modules/umi/node_modules/@umijs/runtime';

const plugin = new Plugin({
  validKeys: ['modifyClientRenderOpts','patchRoutes','rootContainer','render','onRouteChange','__mfsu','dva','getInitialState','initialStateConfig','locale','request','dva_handle_actions','dvaPluginConfig',],
});

export { plugin };

看Plugin的来源@umijs/runtime,意思是这个是运行时插件,我们知道umi中可以配置运行时插件v3.umijs.org/zh-CN/docs/…便是umi提供了注册为运行时插件的机制。上面的代码初始化了一个插件实例,然后导出了插件。

我们找到packages/runtime/Plugin/Plugin.ts

export default class Plugin {
  validKeys: string[];
  hooks: {
    [key: string]: any;
  } = {};

  constructor(opts?: IOpts) {
    this.validKeys = opts?.validKeys || [];
  }
  // 其他代码
}

实例化的过程很简单,就只是记录了validkeys,那validkeys的作用是什么呢?

umi.ts同时还引入了[umi项目临时文件路径]/core/pluginRegister.ts,代码大致如下:

// @ts-nocheck
import { plugin } from './plugin';
import * as Plugin_0 from '../../app.tsx';
import * as Plugin_2 from '../plugin-initial-state/runtime';
import * as Plugin_3 from '../plugin-locale/runtime.tsx';
import * as Plugin_4 from '../plugin-model/runtime';

  plugin.register({
    apply: Plugin_0,
    path: '../../app.tsx',
  });
  plugin.register({
    apply: Plugin_2,
    path: '../plugin-initial-state/runtime',
  });
  plugin.register({
    apply: Plugin_3,
    path: '../plugin-locale/runtime.tsx',
  });
  plugin.register({
    apply: Plugin_4,
    path: '../plugin-model/runtime',
  });

export const __mfsu = 1;

这里注册了一些运行时插件,我们看看注册机制是怎样的,在packages/runtime/Plugin/Plugin.ts

register(plugin: IPlugin) {
    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]);
    });
  }

对比这看,validKeys的作用是记录有效的运行时插件,然后插件key是导出的对象的函数名,相同的key的钩子会组合在一起。这里还是比较简单的,从关联插件plugin-initial-stateplugin-locale中我们可以看到都导出了rootContainer函数,然后被注册成运行时插件。rootContainer钩子基本都是接受一个Element,然后包裹一层自己的组件,最后返回Element,那我们看看插件的使用applyPlugins

 applyPlugins({
    key,
    type,
    initialValue,
    args,
    async,
  }: {
    key: string;
    type: ApplyPluginsType;
    initialValue?: any;
    args?: object;
    async?: boolean;
  }) {
    const hooks = this.getHooks(key) || [];

    switch (type) {
      case ApplyPluginsType.modify:
        if (async) {
          return hooks.reduce(
            async (memo: any, hook: Function | Promise<any> | object) => {
              if (isPromiseLike(memo)) {
                memo = await memo;
              }
              if (typeof hook === 'function') {
                const ret = hook(memo, args);
                if (isPromiseLike(ret)) {
                  return await ret;
                } else {
                  return ret;
                }
              } else {
                if (isPromiseLike(hook)) {
                  hook = await hook;
                }
                return { ...memo, ...hook };
              }
            },
            isPromiseLike(initialValue)
              ? initialValue
              : Promise.resolve(initialValue),
          );
        } else {
          return hooks.reduce((memo: any, hook: Function | object) => {
            if (typeof hook === 'function') {
              return hook(memo, args);
            } else {
              // TODO: deepmerge?
              return { ...memo, ...hook };
            }
          }, initialValue);
        }
      case ApplyPluginsType.event:
        return hooks.forEach((hook: Function) => {
          hook(args);
        });

      case ApplyPluginsType.compose:
        return () => {
          return _compose({
            fns: hooks.concat(initialValue),
            args,
          })();
        };
    }
  }

前面调用rootContainer钩子是ApplyPluginsType.modify且非同步的,那调用的那段代码是

return hooks.reduce((memo: any, hook: Function | object) => {
            if (typeof hook === 'function') {
              return hook(memo, args);
            } else {
              // TODO: deepmerge?
              return { ...memo, ...hook };
            }
}, initialValue);

/* initialValue是
<RouterComponent
        history={opts.history}
        routes={opts.routes}
        plugin={opts.plugin}
        ssrProps={opts.ssrProps}
        defaultTitle={opts.defaultTitle}
/> */

显然rootContainer是逐一调用各个插件暴露出来的rootContainer的钩子,返回最终的rootContainer给到react渲染。这个钩子的作用其实就是包裹组件来提供功能注入的效果,比如model,locale等。简单来说就是插件包裹一层,然后从上层注入自己的参数。那么下层组件既可以消费相关的参数。

相关问题: 1.umi怎么使用react进行代码编译的?

回答:renderClient中将包裹rootContainer组件挂到了根节点。

2.怎么关联上routes的?

回答:调用钩子rootContainer挂载RouterComponent,该组件中实现了路由的渲染。

umi源码阅读4:routes渲染过程

参考资料:

UMI3源码解析系列之构建原理_大转转FE的博客-CSDN博客

umi源码解析

if 我是前端Leader,谈谈前端框架体系建设 - 掘金