umi动态路由、菜单和qiankun

1,099 阅读7分钟

前言

umi的前置知识

umi的路由机制 :umi官方网站

从官方网站中我们知道umi对路由的处理增加了一种约定式路由,也就是说我们从原来的手动配置变为你创建的组件文件夹提取出路由对应的组件形成路由数据。 后者是通过在配置文件中是否设置routes属性来实现的。

我们这里主要说的是前者, 通过我们自己配置路由的方式更加灵活一些。 这里也分了两种方式处理, 第一种我们直接在配置文件中写好我们的路由信息, 第二种就是在app.tsx即入口中动态加载路由方式 。

浏览器的路由信息

由上面的路由我们引入一些基础知识, 浏览器对路由的处理, 以及umi在此基础上做了哪些处理。 我们浏览器的路由分为了两种模式1.hash模式 2.history模式

hash模式的实现

location:当前浏览器url的详情数据 history:当前浏览器的历史url数据

当浏览器中url的hash值发生变化的时候,location.hash值发生变化, 从而触发onhashchange事件在事件中作了一些处理比如加载对应的组件。

history模式

浏览器本身支持前进后退之后加载对应的组件和页面内容展示,也就是说提供的pop、back、forward是会自动触发popState事件,然后加载内容。 另外一个提供的pushState、replaceState只支持修改当前浏览器的url,不支持修改url后自动加载内容,所以根据需要做一些事件处理

言归正传 着手处理动态路由

由上面的一些知识,我们就可以动手来处理动态的路由和菜单了

这里插入一句废话: 通常我们的菜单是由后端来控制, 所以我们在进行配置路由的时候要使用动态的方式来处理

umi动态路由文档

分解动作

修改渲染前的路由数据

let extraRoutes:any = []
export function patchClientRoutes({routes}){
  //根据 extraRoutes 对 routes 做一些修改
  routes.unshift(extraRoutes)
}

渲染操作

export function render(oldRender:any){
  //1.处理extraRoutes数据
  //  1.1 可以请求接口获取菜单和路由信息
  //  1.2 拼接extraRoutes,这里要拼接路由对应的组件
  
  extraRoutes={
    path:'/',
    children:[
      {path: '', element:<Home/>},
      { 
        path: '', 
        element:<Layout menuData={menuData}/>,
        children:[...]
      }
    ],
  }
  
  //渲染
  oldRender();
}

注册子应用和路由

export const qiankun = getRoute().then(({ apps, routes }: any) => {
  return {
    apps: apps,
    routes: routes,
    lifeCycles: {
      beforeLoad: (...args: any) => console.log('beforeLoad', args),
      beforeMount: (...args: any) => console.log('beforeMount', args),
    },
    prefetch: 'all',
  };
});

整体的实现

queryTree的返回值类型如下

{
  "traceId": null,
  "requestId": null,
  "success": true,
  "module": {
    "menuList": [
      {
        "gmtModified": "2023-09-20T06:23:14.000+00:00",
        "code": "home",
        "subMenuList": [
          {
            "gmtModified": "2023-09-20T10:31:24.000+00:00",
            "code": "home_work3",
            "subMenuList": [],
            "sort": 1,
            "title": "首页工作台3",
            "gmtCreate": "2023-09-20T10:31:24.000+00:00",
            "type": "MENU",
            "parentId": 3,
            "url": "/tools",
            "extra": {
              "component": "",
              "pageType": "local",
              "showBread": "false",
              "isQiankun": "false",
              "isHidden": "false",
              "qiankunData":{"name":"hh-dop","preEntry":"https://pre-hh-dop.xxx.com","onlineEntry":"https://hh-dop.xx.com"}
            },
            "id": 8,
            "permissionCode": "home_work3",
            "tenant": "123",
            "status": "ENABLE"
          }
        ],
        "sort": 0,
        "title": "菜单首页",
        "gmtCreate": "2023-09-20T06:23:14.000+00:00",
        "type": "MENU",
        "parentId": 0,
        "url": "/home",
        "extra": {
          "component": "",
          "pageType": "local",
          "isHidden": "false",
          "desc": "这是菜单描述"
        },
        "id": 3,
        "permissionCode": "_home",
        "tenant": "123",
        "status": "ENABLE"
      }
    ]
  },
  "resultCode": 0,
  "resultMsg": null,
  "httpStatusCode": 200
}

app.tsx

import React, { lazy } from 'react';
//@ts-ignore
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import api from './apis';
import { message } from 'antd';
import NOTFOUNE from '@/pages/404';
import { getEnv, getIsLogin } from './utils/utils';

import Layout from '@/layouts/index';
import Home from '@/pages/Home/index';
import RenderFile from './pages/file';
import PartnerApplication from './pages/partner/partnerApplication';

//是否登陆
const isLogin = getIsLogin();
//该应用的路由信息
let menuData: any = {}; 
//未登录时 展示的一个单独页面
let layoutMenuData = {
  name: '工作台',
  path: '/workbench',
  children: [
    {
      name: '合作伙伴',
      path: '/workbench/partner',
      children: [
        {
          name: '入驻申请',
          path: '/workbench/partner/partnerApplication',
          element: <PartnerApplication />,
        },
      ],
    },
  ],
};
let elementList: any = []; //路由绑定引入子应用
let appList: any = []; //待注册子应用
//主要用来判断当前的url有没有对应的组件
const elementSet: any = new Set();
//对面包屑的处理
const breadObj: any = { '/': '工作台', '/workbench': '首页' };

if (isLogin) {
  const context = require.context('./pages', true, /.tsx/);
  context.keys().forEach((k: string) => {
    elementSet.add(k.slice(1));
  });
}

/**
 * 递归迭代处理路由节点 subMenuList -->routes  url-->path title-->name
 * @param node 路由节点
 * @returns 改变后的节点信息
 */
function dfs(node) {
  if (node == null) {
    return;
  }
  const { extra } = node;
  const {
    component: componentProps = '', //本地组件填写的路径是componentProps
    pageType,
    qiankunData,
    isHidden = 'false',
    isQiankun = 'false',
  } = extra || {};

  console.log('pageType', pageType);
  console.log('extra', extra);

  // 不显示菜单
  if (isHidden == 'true') {
    node.hideInMenu = true;
  }

  // 处理面包屑
  breadObj['/workbench' + node.url] = node.title;

  node.routes = node.subMenuList;
  node.path = '/workbench' + node.url;
  node.name = node.title;

  node.children = node.subMenuList;

  // 处理路由对应的组件
  if (pageType == 'local' && isQiankun == 'false') {
    if (elementSet.has(node.url + '/index.tsx')) {
      console.log(' begin import url', node.url + '/index.tsx');
      node.element = <RenderFile fileKey={node.url} />;
      //也可以通过导入组件的方式处理
      //但是这里有一个问题就是导入的组件只有函数组件本身没有上面的依赖,所以会加载错误
      // const PageComp: any = require('./pages' +
      //   node.url +
      //   '/index.tsx').default;
      // if (PageComp) {
      //   console.log(' begin import render component', PageComp);
      //   // node.element = <PageComp />;
      // } else {
      //   node.element = <NOTFOUNE />;
      // }
    }
  }
  if (pageType == 'local' && isQiankun == 'true') {
    // 1.qiankun组件
    //处理注册子应用 todo去重
    const _qiankunData = qiankunData && JSON.parse(qiankunData);
    console.log('_qiankunData', _qiankunData);
    if (_qiankunData && isQiankun == 'true') {
      console.log('_qiankunData', 'name', _qiankunData.name);
      if (getEnv() == 'pre') {
        appList.push({
          name: _qiankunData?.name,
          entry: _qiankunData?.preEntry,
          activeRule: '#/workbench/dop',
        });
      } else {
        appList.push({
          name: _qiankunData?.name,
          entry: _qiankunData?.onlineEntry,
          activeRule: '#/workbench/dop',
        });
      }
    }
    node.microApp = _qiankunData?.name; //匹配对应的组件
  }

  // TODO: 乾坤下面的子组件是否要进行处理
  // if (node.microApp) {
  //   node.exact = false;
  //   if (!isStatic) {
  //     node.routes.forEach((it) => {
  //       delete it.element;
  //     });
  //   }
  // }

  for (let child of node.subMenuList) {
    dfs(child);
  }
}

const getRoute = async () => {
  return new Promise((resolve, reject) => {
    if (isLogin) {
      api.menu
        .queryTree({})
        .then(async (res: any) => {
          const { data } = res;
          console.log('22hello', data?.module);

          let _routes = data?.module?.menuList;
          for (let child of _routes) {
            dfs(child);
          }
          console.log(_routes, '22_routes');
          menuData.path = '/workbench';
          menuData.routes = _routes;

          elementList = _routes;

          console.log(elementList, 'elementList', appList, 'appList');

          resolve({
            // 注册子应用
            apps: appList,
            // 路由绑定子应用 子应用中要有路由映射
            // 注意这里面使用element
            routes: [
              { path: '', element: <Home /> }, // 首页
              {
                path: '/workbench',
                element: <Layout menuData={menuData} breadObj={breadObj} />,
                children: elementList,
              },
            ],
          });
        })
        .catch((err: any) => {
          message.error(err);
        });
    } else {
      resolve({
        apps: [],
        routes: [
          { path: '', element: <Home /> }, // 首页
          {
            path: '/workbench',
            element: <Layout menuData={layoutMenuData} />,
            children: [
              {
                name: '合作伙伴',
                path: '/workbench/partner',
                children: [
                  {
                    name: '合作伙伴入驻申请',
                    path: '/workbench/partner/partnerApplication',
                    element: <PartnerApplication />,
                  },
                ],
              },
            ],
          },
        ],
      });
    }
  });
};

let extraRoutes: any;
export function patchClientRoutes({ routes }) {
  routes.unshift(extraRoutes);
}

export function render(oldRender: any) {
  // TODO: 1.我要在这里拼接路由对应的组件
  // 请求接口获取菜单&路由信息 并赋值到extraRoutes
  if (isLogin) {
    extraRoutes = {
      path: '/',
      children: [
        { path: '', element: <Home /> }, // 首页
        {
          path: '/workbench',
          element: <Layout menuData={menuData} />,
          children: elementList,
        },
      ],
    };
  } else {
    extraRoutes = {
      path: '/',
      children: [
        { path: '', element: <Home /> }, // 首页
        {
          path: '/workbench',
          //将菜单数据采用组件引入的方式, 因为该版本 在layout中获取不到菜单数据
          element: <Layout menuData={menuData} />,
          children: [
            {
              name: '合作伙伴',
              path: '/workbench/partner',
              // element: <PartnerApplication />,
            },
          ],
        },
      ],
    };
  }

  oldRender();
}


// 通过调用getRoutes 然后调用远程的接口,我们获取拼接好路由数据,和qiankun数据,在这里进行注册和绑定 
export const qiankun = getRoute().then(({ apps, routes }: any) => {
  return {
    apps: apps,
    routes: routes,
    lifeCycles: {
      beforeLoad: (...args: any) => console.log('beforeLoad', args),
      beforeMount: (...args: any) => console.log('beforeMount', args),
    },
    prefetch: 'all',
  };
});

layouts/index.tsx

import React, { useEffect, lazy, useState } from 'react';
import ProLayout, { PageContainer } from '@ant-design/pro-layout';
import { Link } from 'umi';
import { Breadcrumb } from 'antd';
import Header from './Header';
import { useLocation, Outlet, connect } from 'umi';
import './index.less';
import { getIsLogin } from '@/utils/utils';
import api from '@/apis';

export interface MenuDataItem {
  authority?: string[] | string;
  children?: MenuDataItem[];
  hideChildrenInMenu?: boolean;
  hideInMenu?: boolean;
  icon?: string;
  locale?: string;
  name?: string;
  path: string;
}

const BasicLayout = (props: any) => {
  console.log('BasicLayout props: ', props);
  const location = useLocation();
  const {
    dispatch,
    globalState,
    children,
    route,
    menuData,
    breadObj,
    ...rest
  } = props;


  const isLogin = getIsLogin();

  //这里是用来处理面包屑的跳转url
  const [pathArr, setPathArr] = useState([]);
  useEffect(() => {
    let _pathArr: any = [];
    let pre = '/';
    location?.pathname &&
      location?.pathname
        ?.slice(1)
        .split('/')
        .map((item) => {
          _pathArr.push(pre + item);
          pre = pre + item + '/';
        });
    setPathArr(_pathArr);
  }, [location]);

  const handleMenuCollapse = () => {};
  return (
    <div
      className="my-layout"
      style={{
        height: '100vh',
      }}
    >
      <ProLayout
        title="伙伴平台"
        menuHeaderRender={(logo: string, title: string) => {
          return (
            <Link to="/">
              <img
                style={{
                  display: 'inline-block',
                  height: 32,
                  verticalAlign: 'middle',
                }}
                src="https://xx.png"
                alt=""
              />
              {title}
            </Link>
          );
        }}
        onCollapse={handleMenuCollapse}
        menuItemRender={(item: any, dom: HTMLElement) => {
          return <Link to={item.path}>{dom}</Link>;
        }}
        menuDataRender={(menuData: MenuDataItem[]) => {
          return menuData.map((item, index) => {
            return {
              ...item,
            };
          });
        }}
        layout={top}
        avatarProps={Header}
        {...rest}
        route={menuData}
      >
      
        {isLogin ? (
          <Breadcrumb
            style={{
              backgroundColor: '#fff',
              padding: '20px',
              marginBottom: '20px',
              borderRadius: '8px',
            }}
          >
            {pathArr.length > 0 &&
              pathArr.map((item) => {
                return (
                  <Breadcrumb.Item key={item}>
                    <a href={`#${item}`} style={{ color: '#5d5d5d' }}>
                      {breadObj[item]}
                    </a>
                  </Breadcrumb.Item>
                );
              })}
          </Breadcrumb>
        ) : null}

        <Outlet />
        {/* <PageContainer header={{ title: null, breadcrumb: undefined }}>
          {children}
        </PageContainer> */}
      </ProLayout>
    </div>
  );
};

export default BasicLayout;

file.tsx

这个文件主要是根据当前的url加载对应的组件的作用,因为在app.tsx中使用require或者import 引入的只是组件的本身,然而组件的相关依赖都没办法处理,有两种解决方案,一种在这里处理,另外一种是 把使用到的组件上面的依赖都在app.tsx中引入 也可以解决

import React from 'react';
import { useLocation } from 'umi';

import MyPermission from '@/pages/authManagement/myPermissions';
import PersonnelManagement from '@/pages/authManagement/personnelManagement';
import Home from '@/pages/Home';

import NOTFOUNE from '@/pages/404';
import { memo } from 'react';

const fileMap = new Map();
// fileMap.set('', Home);
// fileMap.set('/', EntryApplicationRecord);
fileMap.set('/authManagement/myPermissions', MyPermission);
fileMap.set('/authManagement/roleManagement', RoleManagement);

function RenderFile(props: any) {
  console.log('props render file', props);
  const location = useLocation();

  const { fileKey } = props;
  console.log('fileKey', fileKey);
  console.log('filekey location', location.pathname.slice(10));
  console.log('fileKey,comp', fileMap.get(location.pathname.slice(10)));

  let CompPage =
    fileMap.get(location.pathname.slice(10)) && location?.pathname
      ? fileMap.get(location.pathname.slice(10))
      : NOTFOUNE;
  return (
    <>
      <CompPage />
    </>
  );
}
export default memo(RenderFile);

TODO:补上github仓库运行代码