本篇对 doc 项目生成的.umi 进行分析。

待研究

参考 《## umi.ts》
参考 《### renderClient》

.umi 的项目结构

执行 pnpm doc:dev 命令后会生成 .umi 目录 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ treer -d .umi -e aa.txt
.umi
├─exports.ts #这个文件比较特殊 exports 是 umi 的别名,通过 import mui,指向的就是这个文件
├─umi.ts #入口文件
├─plugin-terminal
| └index.ts
├─plugin-docs
| ├─index.ts
| └Layout.tsx
├─core
| ├─EmptyRoute.tsx
| ├─history.ts
| ├─plugin.ts
| ├─pluginConfig.d.ts
| ├─polyfill.ts
| └route.tsx

umi.ts

介绍

umi.ts 对应的 tpl 模板是 packages\preset-umi\templates\umi.tpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// @ts-nocheck
// This file is generated by Umi automatically
// DO NOT CHANGE IT MANUALLY!
import './core/polyfill';

import { renderClient } from 'D:/git/umi/umi-next/packages/renderer-react';
import { getRoutes } from './core/route';
import { createPluginManager } from './core/plugin';
import { createHistory } from './core/history';
import { ApplyPluginsType } from 'umi';


async function render() {
const pluginManager = createPluginManager();
const { routes, routeComponents } = await getRoutes(pluginManager);

// allow user to extend routes
await pluginManager.applyPlugins({
// todo 待研究
key: 'patchRoutes',
type: ApplyPluginsType.event,
args: {
routes,
routeComponents,
},
});

return (pluginManager.applyPlugins({
// todo 待研究
key: 'render',
type: ApplyPluginsType.compose,
initialValue() {
const contextOpts = pluginManager.applyPlugins({
// todo 待研究
key: 'modifyContextOpts',
type: ApplyPluginsType.modify,
initialValue: {},
});
const context = {
routes,
routeComponents,
pluginManager,
rootElement: document.getElementById('root'),
history: createHistory({
// todo 待研究
type: 'browser',
}),
basename: contextOpts.basename || '/',
};
return renderClient(context);
},
}))();
}


render();

其大致的原理 从 getRoutes 中获取 路由组件,以及菜单数据;
然后使用 renderer-react 的 renderClient 将页面渲染出来。

下面对上述过程中涉及的进行说明。

renderer-react 的 renderClient

todo
这个方法挺好,可以以此看下整个 packages\renderer-react\src\browser.tsx 。
看下这个router 以及 顶层如何设计的。

exports.ts

概述

这个文件比较特殊 exports 是 umi 的别名,通过 import mui,指向的就是这个文件。

1
2
3
4
5
6
7
8
9
10
11
12
// @ts-nocheck
// This file is generated by Umi automatically
// DO NOT CHANGE IT MANUALLY!
// @umijs/renderer-*
export { createBrowserHistory, createHashHistory, createMemoryHistory, createSearchParams, Link, matchPath, matchRoutes, Navigate, NavLink, Outlet, useLocation, useMatch, useNavigate, useOutlet, useParams, useResolvedPath, useRoutes, useSearchParams, useAppData, renderClient, useRouteData } from 'D:/git/umi/umi-next/packages/renderer-react';
// umi/client/client/plugin
export { ApplyPluginsType, PluginManager } from 'D:/git/umi/umi-next/packages/umi/client/client/plugin.js';
export { history, createHistory } from './core/history';
// plugins
export { FeatureItem, Features, Hero, Message } from 'D:/git/umi/umi-next/.umi/plugin-docs';
export { terminal } from 'D:/git/umi/umi-next/.umi/plugin-terminal';
// plugins types.d.ts

exports的别名是umi

1
2
3
4
5
6
7
8
9
10
11
// packages\preset-umi\src\features\configPlugins\configPlugins.ts
alias: {
umi: '@@/exports',
react:...,
},

memo.alias = {
...memo.alias,
'@': args.paths.absSrcPath,
'@@': args.paths.absTmpPath,
};

通过preset-umi tmpFiles生成

exports.ts 是通过下面这个文件编译出来的:

1
packages\preset-umi\src\features\tmpFiles\tmpFiles.ts

其实.umi下大部分文件,都是通过上面这个文件生成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// packages\preset-umi\src\features\tmpFiles\tmpFiles.ts

// Generate @@/exports.ts
api.register({
key: 'onGenerateFiles',
fn: async () => {
const rendererPath = winPath(
await api.applyPlugins({
key: 'modifyRendererPath',
initialValue: dirname(
require.resolve('@umijs/renderer-react/package.json'),
),
}),
);

const exports = [];
const exportMembers = ['default'];
// @umijs/renderer-react
exports.push('// @umijs/renderer-*');

exports.push(
`export { ${(
await getExportsAndCheck({
path: join(rendererPath, 'dist/index.js'),
exportMembers,
})
).join(', ')} } from '${rendererPath}';`,
);

// umi/client/client/plugin
exports.push('// umi/client/client/plugin');
const umiDir = process.env.UMI_DIR!;
const umiPluginPath = winPath(join(umiDir, 'client/client/plugin.js'));
exports.push(
`export { ${(
await getExportsAndCheck({
path: umiPluginPath,
exportMembers,
})
).join(', ')} } from '${umiPluginPath}';`,
);
// @@/core/history.ts
exports.push(`export { history, createHistory } from './core/history';`);
checkMembers({
members: ['history', 'createHistory'],
exportMembers,
path: '@@/core/history.ts',
});
// 这里是通过umi插件生成的
// plugins
exports.push('// plugins');
const plugins = readdirSync(api.paths.absTmpPath).filter((file) => {
if (
file.startsWith('plugin-') &&
(existsSync(join(api.paths.absTmpPath, file, 'index.ts')) ||
existsSync(join(api.paths.absTmpPath, file, 'index.tsx')))
) {
return true;
}
});
for (const plugin of plugins) {
let file: string;
if (existsSync(join(api.paths.absTmpPath, plugin, 'index.ts'))) {
file = join(api.paths.absTmpPath, plugin, 'index.ts');
}
if (existsSync(join(api.paths.absTmpPath, plugin, 'index.tsx'))) {
file = join(api.paths.absTmpPath, plugin, 'index.tsx');
}
const pluginExports = await getExportsAndCheck({
path: file!,
exportMembers,
});
if (pluginExports.length) {
exports.push(
`export { ${pluginExports.join(', ')} } from '${winPath(
join(api.paths.absTmpPath, plugin),
)}';`,
);
}
}
// plugins types.ts
exports.push('// plugins types.d.ts');
for (const plugin of plugins) {
const file = winPath(join(api.paths.absTmpPath, plugin, 'types.d.ts'));
if (existsSync(file)) {
// 带 .ts 后缀的声明文件 会导致声明失效
const noSuffixFile = file.replace(/\.ts$/, '');
exports.push(`export * from '${noSuffixFile}';`);
}
}
api.writeTmpFile({
noPluginDir: true,
path: 'exports.ts',
content: exports.join('\n'),
});
},
stage: Infinity,
});

通过源码可知,生成的文件包含以下部分:

  • @umijs/renderer-react 生成的
  • umi/client/client/plugin 生成的
  • @@/core/history.ts 生成的
  • plugins 通过插件生成的 这部分是重点
  • plugins types.ts

通过plugin-docs生成的部分

exports.ts 中,如下部分是哪里生成的,可能会疑惑,其实这部分就是上述 packages\preset-umi\src\features\tmpFiles\tmpFiles.ts 代码中的 【- plugins 通过插件生成的 这部分是重点】 部分生成的。

1
2
3
// exports.ts
// plugins
export { FeatureItem, Features, Hero, Message } from 'D:/git/umi/umi-next/.umi/plugin-docs';

插件定义的组件在这里被集成

由上可知

1
2
// .umi\exports.ts
export { FeatureItem, Features, Hero, Message } from 'D:/git/umi/umi-next/.umi/plugin-docs';

1
2
// .umi\plugin-docs\index.ts
export { FeatureItem, Features, Hero, Message } from 'D:/git/umi/umi-next/packages/plugin-docs/client/theme-doc/index.ts';

/core下的文件哪里生成的

都是preset-umi内生成

1
2
3
4
5
6
7
├─core
| ├─EmptyRoute.tsx
| ├─history.ts
| ├─plugin.ts
| ├─pluginConfig.d.ts
| ├─polyfill.ts
| └route.tsx

上面这些文件的模板都在 目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ treer -e cc.txt
D:\git\umi\umi-next\packages\preset-umi\templates
├─apiRoute.tpl
├─history.tpl
├─middlewares.tpl
├─plugin.tpl
├─route.tpl
├─umi.tpl
├─generate
| ├─api.ts.tpl
| ├─mock.ts.tpl
| ├─page
| | ├─index.less.tpl
| | └index.tsx.tpl
| ├─component
| | ├─component.tsx.tpl
| | └index.ts.tpl

或者没有单独写模板文件,直接写在features\tmpFiles\tmpFiles.ts中:

1
2
3
4
5
6
7
8
9
10
11
12
13
// D:\git\umi\umi-next\packages\preset-umi\src\features\tmpFiles\tmpFiles.ts

// EmptyRoutes.tsx
api.writeTmpFile({
noPluginDir: true,
path: 'core/EmptyRoute.tsx',
content: `
import { Outlet } from 'umi';
export default function EmptyRoute() {
return <Outlet />;
}
`,
});

其文件的模板要么在上述的tpl内,要么就在features\tmpFiles\tmpFiles.ts