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

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

    • 🍕 前言
    • 🍔 前期准备
      • 📃 数据表
      • 🤗 Mock数据
      • 🔗 定义类型
    • 🎈 开始
      • 🎃 获取路由信息
      • 🧵 patchRoutes({ routes, routeComponents})
      • 📸 生成动态路由所需的数据
        • formattedRoutePath
        • routePath
        • componentPath
        • filePath
      • 🍖 生成动态路由数据及组件
      • 😋 完成
    • ✨ 踩坑

🍕 前言

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

🍔 前期准备

📃 数据表

后端同学可以参考

CREATE TABLE `menus` (
  `id` INT(10) NOT NULL AUTO_INCREMENT,
  `menu_id` VARCHAR(128) NOT NULL,
  `parent_id` VARCHAR(128) NULL DEFAULT NULL,
  `enable` TINYINT(1) NOT NULL,
  `name` VARCHAR(64) NOT NULL,
  `sort` SMALLINT(5) NOT NULL DEFAULT '0',
  `path` VARCHAR(512) NOT NULL,
  `direct` TINYINT(1) NULL DEFAULT '0',
  `created_at` DATETIME NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `menu_id` (`menu_id`) USING BTREE,
  UNIQUE INDEX `sort` (`sort`) USING BTREE,
  UNIQUE INDEX `path` (`path`) USING BTREE,
  INDEX `FK_menus_menus` (`parent_id`) USING BTREE,
  CONSTRAINT `FK_menus_menus` FOREIGN KEY (`parent_id`) REFERENCES `menus` (`menu_id`) ON UPDATE CASCADE ON DELETE CASCADE
)
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.ts

export default {
  '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.ts

import type { LazyExoticComponent, ComponentType } from 'react';
import type { Outlet } from '@umijs/max';

declare namespace DynamicRoutes {
  // 后端返回的路由数据为 RouteRaw[]
  interface RouteRaw {
    menuId: string;
    parentId: string;
    enable: boolean;
    name: string;
    sort: number;
    path: string;
    direct: boolean;
    createdAt: string;
  }

  // 前端根据后端返回数据生成的路由数据
  interface Route {
    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),合并路由数据
  interface ParsedRoutes {
    [key: number]: Route;
  }

  // 此类型用于 Object.assign(routeComponents, parsedRoutes),合并路由组件
  interface ParsedRouteComponent {
    [key: number]: RouteComponent;
  }
  
  // parseRoutes 函数的返回值
  interface ParseRoutesReturnType {
    routes: DynamicRoutes.ParsedRoutes;
    routeComponents: DynamicRoutes.ParsedRouteComponent;
  }
}
// ./typing.d.ts
import type { DynamicRoutes } from '@/utils/dynamicRoutes/typing';
import '@umijs/max/typings';

declare global {
  interface Window {
    dynamicRoutes: DynamicRoutes.RouteRaw[];
  }
}

🎈 开始

🎃 获取路由信息

// @/global.ts
import { message } from 'antd';

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

export {};

umi v4.0.24patchRoutes方法早于 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提供的合并路由数据的方法,其参数可以解构出 routesrouteCompoents对象。
routes对象为打平到对象中的路由数据(类型详见DynamicRoutes.Route),routeComponents对象存储routes对象中对应(属性名对应)的组件(类型详见DynamicRoutes.RouteComponent

routes和routesComponents

动态更新路由需要直接修改由参数解构出的 routesrouteComponents对象,使用 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.ts

export function formatRoutePath(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.ts

export function generateRoutePath(path: string) {
  return path.toLowerCase();
}

此函数将使用formatRoutePath转换为全小写字母的路径并提供给DynamciRoutes.Route.path
这个函数根据实际业务需求修改,不必和我一样

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

componentPath

// @/utils/dynamicRoutes/index.ts

export function generateComponentPath(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.ts

export function generateFilePath(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-ignore
export function patchRoutes({ routes, routeComponents }) {
  if (window.dynamicRoutes) {
    // 存在 & 成功获取动态路由数据
    const currentRouteIndex = Object.keys(routes).length; // 获取已在.umirc.ts 中配置的路由数目
    const parsedRoutes = parseRoutes(window.dynamicRoutes, currentRouteIndex);
  }
}

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

// @/utils/dynamicRoutes/index.ts

import type { DynamicRoutes } from './typing';
import { lazy } from 'react';
import { Outlet } from '@umijs/max';

export function parseRoutes(
  routesRaw: DynamicRoutes.RouteRaw[],
  beginIdx: number,
): DynamicRoutes.ParseRoutesReturnType {

  const routes: DynamicRoutes.ParsedRoutes = {}; // 转换后的路由信息
  const routeComponents: DynamicRoutes.ParsedRouteComponent = {}; // 生成的React.lazy组件
  const routeParentMap = new Map<string, number>(); // menuId 与路由记录在 routes 中的键 的映射。如:'role_management' -> 7

  let 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; /Dashboard
    if (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 {
        // 非一级路由
        // 获取父级路由ID
        const 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-ignore
export function patchRoutes({ 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,导致组件路径错误

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/4171.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Endor Labs:2023年十大开源安全风险

近日&#xff0c;Endor Labs发布了一份新报告&#xff0c;确定了2023年的十大开源安全风险。报告显示&#xff0c;许多软件公司依赖于开源软件代码&#xff0c;但在如何衡量和处理与开源软件相关的风险和漏洞方面缺乏一致性。调查发现&#xff0c;在应用程序中超过80%的代码可能…

中文文献怎么查找,带你了解中文文献查找途径及方法

在我们撰写论文和科研工作时经常会查找文献资料&#xff0c;今天带大家了解中文文献查找途径及方法。 查找中文文献常用网站有&#xff1a; 文献党下载器&#xff08;wxdown.org&#xff09;:是一个几乎整合了所有中外文献数据库资源的文献下载平台&#xff0c;因为资源最多&a…

Redis缓存优化

数据库在用户数量多&#xff0c;系统访问量大的时候&#xff0c;系统性能会下降&#xff0c;用户体验差。1.缓存优化作用&#xff1a;1.降低数据库的访问压力2.提高系统的访问性能3.从而提高用户体验实现思路&#xff1a;1.先查询缓存2.如果缓存有数据&#xff0c;直接返回3.如…

【CE实战-Raft】物品栏数量地址定位

▒ 目录 ▒&#x1f6eb; 导读基础教程需求开发环境1️⃣ 单个物品栏物品个数思路步骤验证2️⃣ 所有的物品栏物品个数思路步骤验证&#x1f6ec; 文章小结&#x1f4d6; 参考资料&#x1f6eb; 导读 基础教程 【CE】Mac下的CE教程Tutorial&#xff1a;基础篇&#xff08;1-4…

LAMP架构之zabbix监控(2):zabbix基础操作

目录 一、zabbix监控节点添加和删除 &#xff08;1&#xff09;手动添加 &#xff08;2&#xff09;自动添加 &#xff08;3&#xff09;按照条件批量添加 &#xff08;4&#xff09;使用api工具进行管理 二、针对应用的zabbix监控 一、zabbix监控节点添加和删除 实验说明&a…

Windows Server 2016远程桌面配置全过程

镜像下载 系统镜像网址 本次下载的是 Windows Server 2016 (Updated Feb 2018) (x64) - DVD (Chinese-Simplified) 远程桌面配置 Step 1 在开始菜单搜索服务&#xff0c;打开服务器管理器&#xff0c;点击右上角的管理按钮 Step 2 添加角色控制&#xff0c;点击下一步 S…

Multisim14.3安装包下载及安装教程

[软件大小]: 888 MB [安装环境]: Win11/Win 10 [软件安装包下载]:https://pan.quark.cn/s/1c0217caf24a Multisim是美国国家仪器&#xff08;NI&#xff09;有限公司推出的以Windows为基础的仿真工具&#xff0c;适用于板级的模拟/数字电路板的设计工作 安装步骤 1.选中下载好…

百度地图入门

百度地图官网百度api 进入官网选择javascript API 里面有详细的教程&#xff0c; 跟着教程先登录注册一个个人开发账号 并创建一个应用获取ak 创建时js需要填白名单&#xff0c;如果是在本地运行填写localhost就好了 当你在控制台看到这个界面是代表创建成功了&#xff0c;…

小程序 table组件

最近有在小程序中用table的需求&#xff0c;但是没有找到有符合要求的组件&#xff0c;所以自己弄了一个&#xff0c;能满足基本需求。 组件下载:https://download.csdn.net/download/weixin_67585820/85047405 引入 "usingComponents": {"table": "…

第一章Vue基础

文章目录前端发展史前端三要素JavaScript框架UI框架JavaScript构建工具三端合一什么是VueVue的好处什么是MVVM为什么要使用MVVM环境配置第一个Vue程序声明式渲染模板语法绑定样式数据绑定为什么要实现数据的双向绑定el与data的两种写法条件渲染事件驱动事件的基本用法事件修饰符…

OAuth2协议

OAuth2协议流程图协议角色和流程授权所需信息授权方式授权码模式&#xff08;authorization code&#xff09;参数简化模式密码模式客户端模式授权方式小结流程图 协议角色和流程 user-agent&#xff1a;浏览器或者手机App平台 资源所有者&#xff08;resourc owner&#xff0…

OCR之论文笔记TrOCR

文章目录TrOCR: Transformer-based Optical Character Recognition with Pre-trained Models一. 简介二. TrOCR2.1. Encoder2.2 Decoder2.3 Model Initialiaztion2.4 Task Pipeline2.5 Pre-training2.6 Fine-tuning2.7 Data Augmentation三. 实验3.1 Data3.2 Settings3.2 Resul…

前端直接生成GIF动态图实践

前言去年在博客中发了两篇关于GIF动态生成的博客&#xff0c;GIF图像动态生成-JAVA后台生成和基于FFmpeg的Java视频Mp4转GIF初探&#xff0c;在这两篇博客中都是采用JAVA语言在后台进行转换。使用JAVA的同学经过自己的改造和开发也可以应用在项目上。前段时间有朋友私下问&…

【网络原理10】构造HTTP请求、HTTPS加密

目录 一、构造HTTP请求 ①使用form表单构造HTTP请求&#xff1a; form表单是如何提交的 form提交的缺点 ②基于ajax构造http请求 如何使用Jquery框架 二、HTTPS 运营商劫持 HTTP的加密版本&#xff1a;HTTPS ①对称加密&#xff1a;客户端和服务端使用同一把密钥&…

Android多媒体功能开发(8)——使用VideoView控件播放视频

Android播放视频类主要有两种方式&#xff1a; VideoView控件SurfaceView控件MediaPlayer VideoView是SurfaceView的子类&#xff0c;实际上VideoView相当于SurfaceView MediaPlayer。SurfaceView支持的功能VideoView都支持。也可用VideoViewMediaPlayer的方式播放。 视频播放…

【Azure 架构师学习笔记】-Azure Data Factory (5)-Managed VNet

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Data Factory】系列。 接上文【Azure 架构师学习笔记】-Azure Data Factory (4)-触发器详解-事件触发器 前言 PaaS服务默认都经过公网传输&#xff0c; 这对很多企业而言并不安全&#xff0c;那么就需要对其进行安全改…

深度学习 Day28——利用Pytorch实现好莱坞明星识别

深度学习 Day28——利用Pytorch实现好莱坞明星识别 文章目录深度学习 Day28——利用Pytorch实现好莱坞明星识别一、前言二、我的环境三、前期工作1、导入依赖项设置GPU2、导入数据集3、划分数据集四、调用官方的VGG16模型五、训练模型1、编写训练函数2、编写测试函数3、设置动态…

主机名解析过程

目录 一&#xff1a;主机Ping通Linux&#xff08;id、ip是一个键值对&#xff09; 1.1 给Linux设置别名 1.2 通过Linux名字ping通Linux 1.2.1 修改文件&#xff08;设置键值对&#xff09; 二&#xff1a;Linux去Ping通主机&#xff08;前提关闭防火墙&#xff09; 一&…

昇腾AI机器人发布,12家企业、5家高校签约,昇腾AI开发者创享日全国巡展沈阳首站成功举办

“创未来&#xff0c;享非凡”昇腾AI开发者创享日2023年全国巡回首站活动成功举办&#xff0c;本次活动由辽宁省科技厅指导&#xff0c;由沈阳市科技局、浑南区人民政府、沈阳高新区管理委员会、华为技术有限公司共同主办&#xff0c;沈阳昇腾人工智能生态创新中心承办&#xf…

数据挖掘(作业汇总)

目录 环境配置 实验1 数据 作业2 环境配置 实验开始前先配置环境 以实验室2023安装的版本为例&#xff1a; 1、安装anaconda&#xff1a;&#xff08;anaconda自带Python,安装了anaconda就不用再安装Python了&#xff09; 下载并安装 Anaconda3-2022.10-Windows-x86_64.ex…
最新文章