|
前言前面几篇文章,我们分析了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
|
|