运行时配置(内置的运行时插件)
modifyClientRenderOpts(fn)
修改 clientRender 参数。
let isSubApp = false;export function modifyClientRenderOpts(memo) { return { ...memo, rootElement: isSubApp ? 'sub-root' : memo.rootElement, };}
patchRoutes({ routes })
patchRoutes 运行时插件的功能在于追加或修改路由。其流程为:编译阶段获得配置路由或扫描文件路由,然后在运行时对这些路由进行再处理。
比如在最前面添加一个 /test 路由,
import Test from '@/pages/test'export function patchRoutes({ routes }) { routes.unshift({ path: '/test', exact: true, component: Test, title: 'test' });}
- 直接修改
routes,不需要返回
render(oldRender: Function)
覆写 render,render 运行时插件的功能在于对渲染脚本进行封装。
比如用于渲染之前做权限校验,
import { history } from 'umi';export function render(oldRender) { fetch('/api/auth').then(auth => { if (auth.isLogin) { oldRender() } else { history.push('/login'); oldRender() } });}
onRouteChange({ routes, matchedRoutes, location, action })
onRouteChange 运行时插件的功能在于监听路由变更信息。
比如用于设置标题,
export function onRouteChange({ matchedRoutes }) { if (matchedRoutes.length) { document.title = matchedRoutes.[matchedRoutes.length - 1].route.title || ''; }}
rootContainer(LastRootContainer, args)
修改交给 react-dom 渲染时的根组件。
比如用于在外面包一个 Provider,
export function rootContainer(container) { return React.createElement(ThemeProvider, null, container);}
args 包含:
- routes,全量路由配置
- plugin,运行时插件机制
- history,history 实例
自定义运行时插件
运行时插件也遵循先注册、后使用的流程。运行时插件表现为数组形式,数组项为运行时插件的导出对象。导出对象中的属性可以是函数、或对象、返回对象的 Promise(前者可称为执行类导出,后两者可称为配置类导出,因其用于获取配置项)
要自定义一个运行时插件,需要先自定义一个编译时插件,用于注册运行时插件
注册
addRuntimePlugin添加运行时插件,返回值格式为文件的绝对路径
addRuntimePluginKey添加运行时可配置项。
import { IApi } from "umi";export default (api: IApi, opts) => { // 需要注册的插件所在的文件的绝对路径,也可以不需要,插件函数在约定文件src/app.ts即可 api.addRuntimePlugin(() => require.resolve('../src/plugin/runtime')); // 插件名 会加入到validKeys中 api.addRuntimePluginKey(() => ['myPlugin1', 'myPlugin2']);}
然后在 runtime.ts 是以下内容:
export function myPlugin1(props) {// routes,全量路由配置// plugin,运行时插件机制// history,history 实例
cosnt { history, plugin, routes } = props// do something}export function myPlugin2(props) {
// do something
}
执行
export function rootContainer(container, args) { args.plugin.applyPlugins({ key: 'myPlugin1', type: ApplyPluginsType.event, args }) args.plugin.applyPlugins({ key: 'myPlugin2', type: ApplyPluginsType.event, args }) return React.createElement(Provider, null, container);}
插件生成及运行
入口文件
// path: src/.umi/umi.ts
// umi临时文件// @ts-nocheckimport './core/polyfill';import '@@/core/devScripts';import { plugin } from './core/plugin';import './core/pluginRegister';import { createHistory } from './core/history';import { ApplyPluginsType } from '/Users/pcc/project/umi-share/node_modules/umi/node_modules/@umijs/runtime';import { renderClient } from '/Users/pcc/project/umi-share/node_modules/@umijs/renderer-react/dist/index.js';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();
// ...
通过执行getClientRender函数,返回clientRender。在getClientRender函数内部可以看到有plugin,运行时阶段同样通过插件化plugin.applyPlugins返回渲染需要的render方法。
插件是如何实现的
// path: ~/umi/packages/runtime/src/Plugin/Plugin.ts
// umi源码export default class Plugin { validKeys: string[]; hooks: { [key: string]: any; } = {}; constructor(opts?: IOpts) { // 初始化阶段是就定义好了允许注册hook的key,后续也不允许修改 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) => { // 通过validKeys校验是否允许注册当前插件 assert( this.validKeys.indexOf(key) > -1, `register failed, invalid key ${key} from plugin ${plugin.path}.`, ); if (!this.hooks[key]) this.hooks[key] = []; // 收集hooks this.hooks[key] = this.hooks[key].concat(plugin.apply[key]); }); }
// 获取hook getHooks(keyWithDot: string) { // 支持普通方式注册'test',也支持'a.b.c'的注册方式 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 // 支持modify event compose三种类型hook // export enum ApplyPluginsType { // compose = 'compose', // modify = 'modify', // event = 'event', // }
applyPlugins({ key, type, initialValue, args, async, }: { key: string; type: ApplyPluginsType; initialValue?: any; args?: object; async?: boolean; }) { const hooks = this.getHooks(key) || []; if (args) { assert( typeof args === 'object', `applyPlugins failed, args must be plain object.`, ); } switch (type) { case ApplyPluginsType.modify: if (async) { return hooks.reduce( async (memo: any, hook: Function | Promise<any> | object) => { assert( typeof hook === 'function' || typeof hook === 'object' || isPromiseLike(hook), `applyPlugins failed, all hooks for key ${key} must be function, plain object or Promise.`, ); 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) => { 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); } case ApplyPluginsType.event: return hooks.forEach((hook: Function) => { assert( typeof hook === 'function', `applyPlugins failed, all hooks for key ${key} must be function.`, ); hook(args); }); case ApplyPluginsType.compose: return () => { return _compose({ fns: hooks.concat(initialValue), args, })(); }; } }}
-
Plugin的hook在初始化阶段是就定义好了允许注册hook的key,后续也不允许修改,在register阶段注册插件时会通过validKeys校验是否允许注册当前插件。 -
通过
getHooks获取指定key对应的hook,支持普通方式注册key='test',也支持key='a.b.c'的注册方式 -
调用时
applyPlugins方法获取对应的hooks,并且支持modify、event和compose三种类型的hook
临时文件plugin.ts
// path: src/.umi/core/plugin.ts
// umi临时文件
// @ts-nocheckimport { Plugin } from '/Users/pcc/project/umi-share/node_modules/umi/node_modules/@umijs/runtime';const plugin = new Plugin({ validKeys: ['modifyClientRenderOpts','patchRoutes','rootContainer','render','onRouteChange','__mfsu','getInitialState','initialStateConfig','locale','request',],});export { plugin };
// path: src/.umi/core/pluginRegister.ts
// umi临时文件// @ts-nocheckimport { plugin } from './plugin';import * as Plugin_0 from '../../app.tsx';import * as Plugin_1 from '../plugin-initial-state/runtime';import * as Plugin_2 from '../plugin-model/runtime'; // 默认运行时配置 plugin.register({ apply: Plugin_0, path: '../../app.tsx', }); plugin.register({ apply: Plugin_1, path: '../plugin-initial-state/runtime', }); plugin.register({ apply: Plugin_2, path: '../plugin-model/runtime', });export const __mfsu = 1;
这里实例化了内置的几个运行时插件并注册
分析上面代码,首先会从./core/plugin文件导入实例化好的plugin,同时在./core/pluginRegister中注册plugin,然后在入口执行key=render的hook获取渲染所需的render方法。
plugin.ts和pluginRegister.ts文件如何生成
// path: ~/umi/packages/preset-built-in/src/plugins/generateFiles/core/plugin.ts// umi源码包
export default function (api: IApi) { api.onGenerateFiles(async (args) => { // 调用构建时插件的addRuntimePluginKey的hook获取运行时插件所需的validKeys const validKeys = await api.applyPlugins({ key: 'addRuntimePluginKey', type: api.ApplyPluginsType.add, initialValue: [ 'modifyClientRenderOpts', 'patchRoutes', 'rootContainer', 'render', 'onRouteChange', '__mfsu', ], }); // 调用构建时插件的addRuntimePlugin的hook获取运行时所需的插件 const plugins = await api.applyPlugins({ key: 'addRuntimePlugin', type: api.ApplyPluginsType.add, initialValue: [ getFile({ base: paths.absSrcPath!, fileNameWithoutExt: 'app', type: 'javascript', })?.path, ].filter(Boolean), }); api.writeTmpFile({ path: 'core/plugin.ts', content: Mustache.render( readFileSync(join(__dirname, 'plugin.tpl'), 'utf-8'), { validKeys, runtimePath, }, ), }); api.writeTmpFile({ path: 'core/pluginRegister.ts', content: Mustache.render( readFileSync(join(__dirname, 'pluginRegister.tpl'), 'utf-8'), { plugins: plugins.map((plugin: string, index: number) => { return { index, path: winPath(plugin), }; }), }, ), }); }); // ...}
分析生成文件plugin.ts的插件,我们可以看到这个插件主要做了4件事:
-
调用构建时插件的
addRuntimePluginKey的hook获取运行时插件所需的validKeys注:我们开发自定义插件时,可以通过
addRuntimePluginKey注册自定义的validKey -
调用构建时插件的
addRuntimePlugin的hook获取**运行时所需的插件注:我们可以看到运行时插件的默认值
initialValue是从src/app文件中获取的,也就是在src/app文件中注册我们所需的hook,例如:通过patchRoutes更改路由、通过onRouteChange设置标题等操作 -
读取
plugin.tpl模版,生成临时文件plugin.ts -
读取
pluginRegister.tpl模版,生成临时文件pluginRegister.ts
renderClient
// path: ~/umi/packages/renderer-react/src/renderClient/renderClient.tsx
export default function renderClient(opts: IOpts) { const rootContainer = getRootContainer(opts); if (opts.rootElement) { const rootElement = typeof opts.rootElement === 'string' ? document.getElementById(opts.rootElement) : opts.rootElement; const callback = opts.callback || (() => {}); // flag showing SSR succeed if (window.g_useSSR) { if (opts.dynamicImport) { // dynamicImport should preload current route component // first loades); preloadComponent(opts.routes).then(function () { hydrate(rootContainer, rootElement, callback); }); } else { hydrate(rootContainer, rootElement, callback); } } else { render(rootContainer, rootElement, callback); } } else { return rootContainer; }}
export default function getRootContainer(opts: IOpts) { return 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, }, });}
分析renderClient方法主要做了2件事:
- 触发
rootContainer运行时阶段的hook,获取rootContainer组件 - 获取
rootElementDOM元素 rootContainer作为根组件,rootElement作为根元素,执行render方法,挂载组件,此时的render方法也就是react-dom导出的render方法
接下来,我们再看下rootContainer组件的initialValue:
export default function RouterComponent(props: IRouterComponentProps) { const { history, ...renderRoutesProps } = props; useEffect(() => { // first time using window.g_initialProps // switch route fetching data, if exact route reset window.getInitialProps if ((window as any).g_useSSR) { (window as any).g_initialProps = null; } function routeChangeHandler(location: any, action?: string) { const matchedRoutes = matchRoutes( props.routes as RouteConfig[], location.pathname, ); // Set title if ( typeof document !== 'undefined' && renderRoutesProps.defaultTitle !== undefined ) { document.title = (matchedRoutes.length && // @ts-ignore matchedRoutes[matchedRoutes.length - 1].route.title) || renderRoutesProps.defaultTitle || ''; }
// 触发 onRouteChange 对应的 hook
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>;}
这里RouterComponent返回的Router也就是react-router-dom包中导出的Router组件,同时在RouterComponent组件内部监听history,在history发生变化时触发运行时阶段的onRouteChange对应的hook,此时也就可以触发我们自己在业务代码中订阅的onRouteChange的回调了,例如:监听路由改变时,发送埋点或者改变document.title。