umi插件开发仿dumi项目实现页面布局详解

2023-01-30 10:05:18
目录
实现思路使用默认项目提供的layout文件自定义主题准备工作主题插件功能modifyAppData插件代码生成layout路由对象使用同步伪代码来描述上面流程运行检查

实现思路

上一章我们已经完成/docs目录下文件自动生路由功能,本章我们将在此基础上,实现自动生成页面导航的功能。

    使用默认模板提供的layout展示路由切换使用自定义主题插件

    使用默认项目提供的layout文件

    在我们创建默认umi项目后,会在/src/layouts下生成一个布局文件:

    同时在上一章节我们打印modifyRouteshook的入参,可以看到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:>让主题插件的modifyAppDataappData插件的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来生成一个布局路由,并添加到返回值中即可,这样所有parentIdDocLayout.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文件。

      以上就是umi插件开发仿dumi项目实现页面布局详解的详细内容,更多关于umi插件仿dumi页面布局的资料请关注易采站长站其它相关文章!