umi阅读分为5个部分,分别是:
以下代码以umi的3.5.20版本为例,主要内容以源码+个人解读为主
渲染过程
已知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-state、plugin-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,该组件中实现了路由的渲染。
参考资料: