0


Umi4 从零开始实现动态路由、动态菜单

Umi4 从零开始实现动态路由、动态菜单

🍕 前言

近期在写 Umi4 的练习项目,计划实现一个从服务器获取路由信息并动态生成前端路由和导航菜单的功能。本文记录了相关知识、思路以及开发过程中踩到的坑。

🍔 前期准备

📃 数据表

后端同学可以参考

CREATETABLE`menus`(`id`INT(10)NOTNULLAUTO_INCREMENT,`menu_id`VARCHAR(128)NOTNULL,`parent_id`VARCHAR(128)NULLDEFAULTNULL,`enable`TINYINT(1)NOTNULL,`name`VARCHAR(64)NOTNULL,`sort`SMALLINT(5)NOTNULLDEFAULT'0',`path`VARCHAR(512)NOTNULL,`direct`TINYINT(1)NULLDEFAULT'0',`created_at`DATETIMENOTNULL,PRIMARYKEY(`id`)USINGBTREE,UNIQUEINDEX`menu_id`(`menu_id`)USINGBTREE,UNIQUEINDEX`sort`(`sort`)USINGBTREE,UNIQUEINDEX`path`(`path`)USINGBTREE,INDEX`FK_menus_menus`(`parent_id`)USINGBTREE,CONSTRAINT`FK_menus_menus`FOREIGNKEY(`parent_id`)REFERENCES`menus`(`menu_id`)ONUPDATECASCADEONDELETECASCADE)COLLATE='utf8mb4_0900_ai_ci'ENGINE=InnoDB;

在这里插入图片描述

id 记录ID
menu_id 菜单的唯一ID
parent_id 父级菜单的ID
enable 是否启用菜单(后端或查询时进行过滤)
name 路由名称、菜单名称、页面标题
sort 菜单排序(后端或查询时进行排序)(xxxx 代表:一级菜单序号 子菜单序号)
path 前端页面访问路径(同location.pathname)
direct 是否为直接访问的菜单(即不存在子菜单和子路由,为顶级项目)
created_at 记录创建时间

🤗 Mock数据

// ./mock/dynamicRoutes.tsexportdefault{'POST /api/system/routes':{"code":200,"msg":"请求成功","data":[{"id":1,"menuId":"dashboard","parentId":"","enable":true,"name":"仪表盘","sort":1000,"path":"/dashboard","direct":true,"createdAt":"1992-08-17 07:29:03"},{"id":2,"menuId":"system_management","parentId":"","enable":true,"name":"系统管理","sort":2000,"path":"/system","direct":false,"createdAt":"2011-01-21 09:25:49"},{"id":3,"menuId":"user_management","parentId":"system_management","enable":true,"name":"用户管理","sort":2001,"path":"/system/user","direct":false,"createdAt":"1986-06-03 02:38:12"},{"id":4,"menuId":"role_management","parentId":"system_management","enable":true,"name":"角色管理","sort":2002,"path":"/system/role","direct":false,"createdAt":"1986-06-03 02:38:12"},{"id":5,"menuId":"permission_management","parentId":"system_management","enable":true,"name":"权限管理","sort":2003,"path":"/system/permission","direct":false,"createdAt":"1986-06-03 02:38:12"},{"id":6,"menuId":"app_management","parentId":"system_management","enable":true,"name":"应用管理","sort":2004,"path":"/system/app","direct":false,"createdAt":"1986-06-03 02:38:12"}]}}

🔗 定义类型

// @/utils/dynamicRoutes/typing.d.tsimporttype{ LazyExoticComponent, ComponentType }from'react';importtype{ Outlet }from'@umijs/max';declarenamespace DynamicRoutes {// 后端返回的路由数据为 RouteRaw[]interfaceRouteRaw{
    menuId:string;
    parentId:string;
    enable:boolean;
    name:string;
    sort:number;
    path:string;
    direct:boolean;
    createdAt:string;}// 前端根据后端返回数据生成的路由数据interfaceRoute{
    id:string;
    parentId:'ant-design-pro-layout'|string;
    name:string;
    path:string;
    file?:string;
    children?: Route[];}// 前端根据后端返回数据生成的React.lazy懒加载组件或Outlet(一级路由)type RouteComponent = LazyExoticComponent<ComponentType<any>>|typeof Outlet;// patchRoutes 函数的参数可以解构出 { routes, routeComponents }// 此类型用于 Object.assign(routes, parsedRoutes),合并路由数据interfaceParsedRoutes{[key:number]: Route;}// 此类型用于 Object.assign(routeComponents, parsedRoutes),合并路由组件interfaceParsedRouteComponent{[key:number]: RouteComponent;}// parseRoutes 函数的返回值interfaceParseRoutesReturnType{
    routes: DynamicRoutes.ParsedRoutes;
    routeComponents: DynamicRoutes.ParsedRouteComponent;}}
// ./typing.d.tsimporttype{ DynamicRoutes }from'@/utils/dynamicRoutes/typing';import'@umijs/max/typings';declare global {interfaceWindow{
    dynamicRoutes: DynamicRoutes.RouteRaw[];}}

🎈 开始

🎃 获取路由信息

// @/global.tsimport{ message }from'antd';try{const{ data: routesData }=awaitfetch('/api/system/routes',{
    method:'POST',}).then((res)=> res.json());if(routesData){
    window.dynamicRoutes = routesData;}}catch{
  message.error('路由加载失败');}export{};

umi v4.0.24

patchRoutes

方法早于

render

方法执行,所以

umi v3

中在

render

函数中获取路由数据的方法目前不可用。不清楚这个行为属于bug还是 umi 4的特性

我在Github提的issue: [Bug] umi 4 运行时配置中 patchRoutes 早于 render 执行 #9486

经过测试,

global.tsx

中的代码早于

patchRoutes

执行,所以在此文件中获取数据。

由于执行

global.tsx

时,

app.tsx

中的运行时响应/请求拦截器还未生效,使用

@umijs/max

提供的

request

会报错,所以这里使用

fetch

获取数据,并写入

window.dynamicRoutes

🧵 patchRoutes({ routes, routeComponents})

此函数为

umi v4

提供的合并路由数据的方法,其参数可以解构出

routes

routeCompoents

对象。

routes

对象为打平到对象中的路由数据(类型详见

DynamicRoutes.Route

),

routeComponents

对象存储

routes

对象中对应(属性名对应)的组件(类型详见

DynamicRoutes.RouteComponent

routes和routesComponents

动态更新路由需要直接修改由参数解构出的

routes

routeComponents

对象,使用

Object.assign(routes, newRoutes)

将他们与新数据合并

📸 生成动态路由所需的数据

以下三处需要使用

DynamicRoutes.RouteRaw.path

经过格式化后的路径:

  1. DynamicRoutes.Route.file在路由信息中记录组件文件位置
  2. DynamciRoutes.Route.path在路由信息中记录组件的路由路径
  3. React.lazy(() => import(path))懒加载组件所需的文件路径

要生成的路径:

  • formattedRoutePath
  • routePath
  • componentPath
  • filePath

formattedRoutePath

// @/utils/dynamicRoutes/index.tsexportfunctionformatRoutePath(path:string){const words = path.replace(/^\//,'').split(/(?<=\w+)\//);// 提取路径单词return`/${words
    .map((word:string)=>
      word.toLowerCase().replace(word[0], word[0].toUpperCase()),).join('/')}`;}

约定使用

@/pages/Aaaa/pages/Bbbb

文件夹结构存储组件

DynamicRoutes.RouteRaw.path

中,路径字母大小写可能是不同的,首先使用此方法将大小写不一的路径转换为单词首字母大写的路径,供其他方法进行下一步转换。

转换前:/SYSTEM/user
转换后:/System/User

routePath

// @/utils/dynamicRoutes/index.tsexportfunctiongenerateRoutePath(path:string){return path.toLowerCase();}

此函数将使用

formatRoutePath

转换为全小写字母的路径并提供给

DynamciRoutes.Route.path

这个函数根据实际业务需求修改,不必和我一样

转换前:/System/User
转换后:/system/user

componentPath

// @/utils/dynamicRoutes/index.tsexportfunctiongenerateComponentPath(path:string){const words = path.replace(/^\//,'').split(/(?<=\w+)\//);// 提取路径单词return`${words.join('/pages/')}/index`;}

此函数生成

React.lazy(() => import(path))

所需路径,用于懒加载组件。但此方法生成的不是完整组件路径,由于

webpack alias

处理机制,需要在

() => import(path)

的参数中编写一个模板字符串

@/pages/${componentPath}

,直接传递将导致@别名失效无法正常加载组件

// 转换前:/System/User// 转换后:/System/pages/User/index
React.lazy(()=>import(`@/pages/${componentPath}`))// 使用时

filePath

// @/utils/dynamicRoutes/index.tsexportfunctiongenerateFilePath(path:string){const words = path.replace(/^\//,'').split(/(?<=\w+)\//);return`@/pages/${words.join('/pages/')}/index.tsx`;}

此函数生成

DynamicRoutes.Route.file

所需的完整组件路径

转换前:/System/User
转换后:@/pages/System/pages/User/index.tsx

🍖 生成动态路由数据及组件

首先,在

app.tsx

中生成

patchRoutes

方法,并获取已在

.umirc.ts

中配置的路由数目

// @/app.tsx// @ts-ignoreexportfunctionpatchRoutes({ routes, routeComponents }){if(window.dynamicRoutes){// 存在 & 成功获取动态路由数据const currentRouteIndex = Object.keys(routes).length;// 获取已在.umirc.ts 中配置的路由数目const parsedRoutes =parseRoutes(window.dynamicRoutes, currentRouteIndex);}}

传入parseRoutes函数,生成路由数据

// @/utils/dynamicRoutes/index.tsimporttype{ DynamicRoutes }from'./typing';import{ lazy }from'react';import{ Outlet }from'@umijs/max';exportfunctionparseRoutes(
  routesRaw: DynamicRoutes.RouteRaw[],
  beginIdx:number,): DynamicRoutes.ParseRoutesReturnType {const routes: DynamicRoutes.ParsedRoutes ={};// 转换后的路由信息const routeComponents: DynamicRoutes.ParsedRouteComponent ={};// 生成的React.lazy组件const routeParentMap =newMap<string,number>();// menuId 与路由记录在 routes 中的键 的映射。如:'role_management' -> 7let currentIdx = beginIdx;// 当前处理的路由项的键。把 patchRoutes 传进来的 routes 看作一个数组,这里就是元素的下标。

  routesRaw.forEach((route)=>{let effectiveRoute =true;// 当前处理中的路由是否有效const formattedRoutePath =formatRoutePath(route.path);// 将服务器返回的路由路径中的单词转换为首字母大写其余小写const routePath =generateRoutePath(formattedRoutePath);// 全小写的路由路径const componentPath =generateComponentPath(formattedRoutePath);// 组件路径 不含 @/pages/const filePath =generateFilePath(formattedRoutePath);// 路由信息中的组件文件路径// 是否为直接显示(不含子路由)的路由记录,如:/home; /Dashboardif(route.direct){// 生成路由信息const tempRoute: DynamicRoutes.Route ={
        id: currentIdx.toString(),
        parentId:'ant-design-pro-layout',
        name: route.name,
        path: routePath,
        file: filePath,};// 存储路由信息
      routes[currentIdx]= tempRoute;// 生成组件const tempComponent =lazy(()=>import(`@/pages/${componentPath}`));// 存储组件
      routeComponents[currentIdx]= tempComponent;}else{// 判断是否非一级路由if(!route.parentId){// 正在处理的项为一级路由// 生成路由信息const tempRoute: DynamicRoutes.Route ={
          id: currentIdx.toString(),
          parentId:'ant-design-pro-layout',
          name: route.name,
          path: routePath,};// 存储路由信息
        routes[currentIdx]= tempRoute;// 一级路由没有它自己的页面,这里生成一个Outlet用于显示子路由页面const tempComponent = Outlet;// 存储Outlet
        routeComponents[currentIdx]= tempComponent;// 记录菜单ID与当前项下标的映射
        routeParentMap.set(route.menuId, currentIdx);}else{// 非一级路由// 获取父级路由IDconst realParentId = routeParentMap.get(route.parentId);if(realParentId){// 生成路由信息const tempRoute: DynamicRoutes.Route ={
            id: currentIdx.toString(),
            parentId: realParentId.toString(),
            name: route.name,
            path: routePath,
            file: filePath,};// 存储路由信息
          routes[currentIdx]= tempRoute;// 生成组件const tempComponent =lazy(()=>import(`@/pages/${componentPath}`));// 存储组件
          routeComponents[currentIdx]= tempComponent;}else{// 找不到父级路由,路由无效,workingIdx不自增
          effectiveRoute =false;}}}if(effectiveRoute){// 当路由有效时,将workingIdx加一
      currentIdx +=1;}});return{
    routes,
    routeComponents,};}

app.tsx

中合并处理后的路由数据

// @ts-ignoreexportfunctionpatchRoutes({ routes, routeComponents }){if(window.dynamicRoutes){const currentRouteIndex = Object.keys(routes).length;const parsedRoutes =parseRoutes(window.dynamicRoutes, currentRouteIndex);
    Object.assign(routes, parsedRoutes.routes);// 参数传递的为引用类型,直接操作原对象,合并路由数据
    Object.assign(routeComponents, parsedRoutes.routeComponents);// 合并组件}}

😋 完成

在这里插入图片描述

✨ 踩坑

  • 目前需要在global.tsx中获取路由数据,因为patchRoutes发生于render之前
  • patchRoutes的原始路由数据与新数据需要使用Object.assign合并,不能直接赋值
  • 使用React.lazy生成懒加载组件时,不能直接传入完整路径。传入完整路径使webpack无法处理alias,导致组件路径错误

本文转载自: https://blog.csdn.net/m0_52761633/article/details/127167701
版权归原作者 绿胡子大叔 所有, 如有侵权,请联系我们删除。

“Umi4 从零开始实现动态路由、动态菜单”的评论:

还没有评论