umi插件开发仿dumi项目实现页面布局详解
kukiiu 人气:0实现思路
上一章我们已经完成/docs
目录下文件自动生路由功能,本章我们将在此基础上,实现自动生成页面导航的功能。
- 使用默认模板提供的layout展示路由切换
- 使用自定义主题插件
使用默认项目提供的layout文件
在我们创建默认umi
项目后,会在/src/layouts
下生成一个布局文件:
同时在上一章节我们打印modifyRoutes
hook的入参,可以看到umi
会将该文件转成一个layout router对象,如图所示:
因此我们可以直接将这个layout的id
属性,赋值到自动生成的路由的parentId
属性上,并添加该对象到返回值中:
// /src/features/routes.ts import path from 'path'; import type { IApi } from 'umi'; import type { IRoute } from '@umijs/core/dist/types'; import { getConventionRoutes } from '@umijs/core'; export default (api: IApi) => { api.describe({ key: 'domi:routes' }); api.modifyRoutes((oRoutes: Record<string, IRoute>) => { const routes: Record<string, IRoute> = {} const docDir = 'docs' // 获取某个目录下所有可以配置成umi约定路由的文件 const dirRoutes: Record<string, IRoute> = getConventionRoutes({ base: path.join(api.cwd, docDir), }); // 默认提供的布局layout的Id let docLayoutId : undefined | string = '@@/global-layout'; // 从旧路由对象中获取放入返回值中 routes[docLayoutId] = oRoutes[docLayoutId] Object.entries(dirRoutes).forEach(([key, route]) => { // 这里将文件的路径改为绝对路径,否则umi会默认找/src/pages下组件 route.file = path.resolve(docDir, route.file); // 给页面对象赋值布局Id route.parentId = docLayoutId routes[route.id] = route; }); return routes; }); };
同时我们修改布局文件,将导航改成我们的测试页面路由:
// /src/layouts/index.tsx import { Link, Outlet } from 'umi'; import styles from './index.less'; export default function Layout() { return ( <div className={styles.navs}> <ul> <li><Link to="/">Home</Link></li> <li><Link to="/button">Button</Link></li> </ul> <Outlet /> </div> ); }
运行项目可以看到布局文件已添加到页面中,并可以切换路由:
自定义主题
上面我们通过最简单的方式使用了默认提供的布局文件,这种方式对页面局限性比较大,由于各个项目对页面的展示要求不一样,dumi
提供了主题插件来灵活扩展用户自定义布局。同时提供了默认主题,用户可以选择性覆盖默认样式。
本节我们将实现其中的默认主题加载
准备工作
创建主题插件,并注册到插件配置中
mkdir /src/features/theme.ts
import { defineConfig } from "umi"; export default defineConfig({ plugins: [ './src/features/routes.ts', './src/features/theme.ts', ], });
创建默认主题目录,将/src/layouts/index.tsx
文件复制到这里
mkdir /src/client/theme-default/layouts/DocLayout cp /src/layouts/index.tsx /src/client/theme-default/layouts/DocLayout/index.tsx cp /src/layouts/index.less /src/client/theme-default/layouts/DocLayout/index.less
然后/src/layouts
就没用了,可以删掉
主题插件功能
dumi
主题插件主要提供的功能有:
- 加载默认主题布局文件
- 加载国际化语言包
- 合并默认主题及自定义主题
- ...等其他与页面相关功能
本章我们将实现默认布局的加载,并配置到路由中。
modifyAppData
umi
提供modifyAppData
钩子,用于初始收集应用数据,在dumi
中使用这个钩子来初始化主题数据。
我们在主题插件中提供的主题数据后续会被用在修改路由中,即在modifyRoutes
阶段使用。因为modifyRoutes
是在appData
插件的modifyAppData
阶段中执行,所以通过before: 'appData
让主题插件的modifyAppData
在appData
插件的modifyAppData
之前先初始化完成,这样在modifyRoutes
中就可以使用到主题数据。
插件代码
// /src/features/theme.ts import path from 'path'; import type { IApi } from 'umi'; import { glob, winPath } from 'umi/plugin-utils'; const DEFAULT_THEME_PATH = path.join(__dirname, '../../src/client/theme-default'); export default async(api: IApi) => { api.describe({ key: 'domi:theme' }); api.modifyAppData({ before: 'appData', async fn(memo: any) { const defaultThemeData = loadTheme(DEFAULT_THEME_PATH); // @ts-ignore api.service.themeData = defaultThemeData return memo; }, }); } /** * 加载主题信息 */ function loadTheme(dir: string) { return { name: path.basename(dir), path: dir, layouts: getComponentMapFromDir( 'layouts/{GlobalLayout,DocLayout,DemoLayout}{.,/index.}{js,jsx,ts,tsx}', dir, ), }; }; /** * 提取dir目录下符合条件的组件信息 */ function getComponentMapFromDir(globExp: string, dir: string) { return glob .sync(globExp, { cwd: dir }) .reduce<any>((ret, file) => { const specifier = path.basename( winPath(file).replace(/(\/index)?\.[a-z]+$/, ''), ); // ignore non-component files if (/^[A-Z\d]/.test(specifier)) { ret[specifier] = { specifier, source: winPath(path.join(dir, file)), }; } return ret; }, {}); }
另一个比较不优雅的地方是这里使用了api.service
来存储生成的主题数据,同样因为上面提到的阶段问题,modifyRoutes
是在modifyAppData
中执行,所以这里只能用全局变量来存储,否则在修改路由阶段拿不到这里的数据。
执行完成后,api.service.themeData
就得到了主题相关的数据:
{ name: 'theme-default', path: 'D:\\project\\domi\\src\\client\\theme-default', layouts: { DocLayout: { specifier: 'DocLayout', source: 'D:/project/domi/src/client/theme-default/layouts/DocLayout/index.tsx' } } }
生成layout路由对象
前面我们使用了模板自带的对象@@/global-layout
作为布局模板,现在我们可以将它改成动态添加主题布局。
我们直接在路由插件中使用主题插件中生成的布局数据,代码很简单,根据前面的layout来生成一个布局路由,并添加到返回值中即可,这样所有parentId
为DocLayout.specifier
的页面就能使用该布局了。
// /src/features/routes.ts ... let docLayoutId : undefined | string = undefined; // @ts-ignore const { DocLayout } = api.service.themeData.layouts; // 从旧路由对象中获取放入返回值中 if (DocLayout) { docLayoutId = DocLayout.specifier; routes[DocLayout.specifier] = { id: DocLayout.specifier, path: '/', file: DocLayout.source, parentId: undefined, absPath: '/', isLayout: true, }; } ...
使用同步伪代码来描述上面流程
// /umi/packages/core/src/service/service.ts 中代码 service.collectAppData() { // /src/features/theme.ts中代码 // 配置before: 'appData' 使其先于appData.modifyAppData执行 themePlugin.modifyAppData() { // 这里加载主题数据 api.service.themeData = loadTheme(DEFAULT_THEME_PATH); } // /umi/packages/preset-umi/src/features/appData/appData.ts 中代码 appDataPlugin.modifyAppData() { // /src/features/routes.ts中代码 routesPlugin.modifyRoutes() { // 这里使用主题数据生成布局路由 const { DocLayout } = api.service.themeData.layouts; routes[DocLayout.specifier] = { ... isLayout: true, }; } } }
运行检查
至此我们已经完成生成默认布局,实现了简易的主题插件,运行代码可以看到和上节运行结果一样:
路由和页面问题基本解决了,接下来就要开始正式解析markdown
文件。
加载全部内容