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

news2025/7/17 8:22:19

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.coloradmin.cn/o/406422.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第二十一篇 vue组件引入使用

这篇内容是关于组件在Vue中的引入和使用&#xff1b;什么是组件以及做组件化的开发&#xff0c;在Vue中&#xff0c;组件是构成页面中独立结构单元&#xff0c;能够减少重复代码的编写&#xff0c;提高开发效率&#xff0c;降低代码耦合度&#xff0c;便于维护和管理&#xff1…

前端请求中body和query传参

前端发送请求最常用的是get请求和post请求 get请求只能传query参数&#xff0c;query参数都是拼接在请求地址上的。 post可以传body和query两种形式的参数。 get请求在url中传送的参数是有长度限制的&#xff0c;而post没有限制。 get比post更不安全&#xff0c;因为参数直接…

十、Echart图表 之 dataZoom区域缩放 基本使用与配置大全

&#x1f353; 作者主页&#xff1a;&#x1f496;仙女不下凡&#x1f496; &#x1f353; 前言介绍&#xff1a;以下&#x1f447;内容是我个人对于该技术的总结&#xff0c;如有不足与错误敬请指正&#xff01; &#x1f353;Echart官网地址&#xff1a;https://echarts.ap…

layui-icon各种常用动态图标

<buttonid"btnPrintDetail"class"<br/>layui-btnicon-btn"><iclass"<br/>layui-icon">&#xe66d;</i>明细打印</button> icon很多&#xff0c;选择起来纷繁复杂&#xff0c;罗列一点常用的&#xff0c;用…

uniapp scroll-view基础用法

前言 在uniapp日常开发的过程中经常会有局部滚动的需求&#xff0c;而scroll-view组件正好可以满足这一需求。需注意在webview渲染的页面中&#xff0c;区域滚动的性能不及页面滚动。 纵向滚动 将scroll-view组件中的属性scroll-y设定为true开启纵向滚动功能&#xff0c;给scr…

【vue2】axios请求与axios拦截器的使用详解

&#x1f973;博 主&#xff1a;初映CY的前说(前端领域) &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;当我们在路由跳转前与后我们可实现触发的操作 【前言】ajax是一种在javaScript代码中发请…

SAP ABAP——SAP简介(三)【S/4 HANA前端显示界面】

&#x1f482;作者简介&#xff1a; Thunder Wang&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学在读&#xff0c;同时任汉硕云&#xff08;广东&#xff09;科技有限公司ABAP开发顾问。在学习工作中&#xff0c;我通常使用偏后端的开发语言AB…

Ubuntu18.04安装Nvidia驱动【全网不坑,超全步骤】(亲测~)

Ubuntu18.04安装Nvidia驱动【全网不坑&#xff0c;超全步骤】亲测&#xff5e;为了方便以后回忆以及给像我一样的菜鸡提供思路&#xff0c;给出具体的步骤&#xff1a;No.1 查看自己的电脑显卡型号&#xff08;已知麻烦自动略过&#xff09;No.2 下载电脑对应的Nvidia版本驱动N…

Vue脚手架报错:‘v-model‘ directives require no argument 解决方案

1、报错&#xff1a; v-model directives require no argument 截图 2、原因&#xff1a; ESLint对vetur进行了eslint检查 3、解决方法 ① 修改模板中使用v-show 将 v-model:show"show" 改为 v-model"show" ② vetur插件的作者给出了解决办法 我们可…

【一起学Rust | 框架篇 | Viz框架】轻量级 Web 框架——Viz

文章目录前言特点一、Hello Viz1. 创建项目2. 引入viz3. 运行Hello Viz4. 运行结果注意二、Hello Viz代码详解导入组件处理请求主函数三、常见用法简单的处理程序实现处理程序特质路由传参链式组合程序中间件参数接收器路由一个简单的路由CRUD操作资源总结前言 Viz&#xff0c…

TS的类型声明

目录 1.TS把JS变成了静态类型的语言&#xff0c;可以给变量指定类型 2.JS中的函数是不考虑参数的类型和个数的&#xff0c;但是TS会考虑函数的参数类型和个数&#xff0c;且要规定返回值类型。 3.常见的TS类型 1.可以直接使用字面量进行类型声明 字面量声明的应用:可以使用…

讲解HTML和CSS(超详细)

讲解HTML和CSS&#xff08;超详细&#xff09;一、初始HTML和CSS1.html的发展历史2.html3.css二、HTML1.基本框架2.HTML5常用的标签和属性三、CSS1.css的使用2.css选择器总结提示&#xff1a;以下是我的一些经验之谈&#xff0c;下面案例可供参考 一、初始HTML和CSS 1.html的…

手撕前端面试题【javascript~ 总成绩排名、子字符串频次统计、继承、判断斐波那契数组等】

前端JavaScript面试题&#x1f353;&#x1f353;总成绩排名&#x1f353;&#x1f353;子字符串频次&#x1f353;&#x1f353;继承&#x1f353;&#x1f353;判断斐波那契数组&#x1f353;&#x1f353;js中哪些操作会造成内存泄漏?html页面的骨架&#xff0c;相当于人的…

Vue中的过滤器(filter)

一、Vue中的过滤器是什么 过滤器&#xff08;filter&#xff09;是输送介质管道上不可缺少的一种装置,大白话&#xff0c;就是把一些不必要的东西过滤掉,过滤器实质不改变原始数据&#xff0c;只是对数据进行加工处理后返回过滤后的数据再进行调用处理&#xff0c;我们也可以理…

Java利用fastjson解析复杂嵌套json字符串、json数组;json字符串转Java对象,json数组转list数组

文章目录前言一、什么是JSON对象&#xff1f;二、什么是json字符串&#xff1f;二、什么是JSON数组&#xff1f;三、复杂、嵌套的json字符串四、json字符串转换4.1 简单json字符串转换为java对象4.2 简单json字符串数组转换为list数组4.3 复杂嵌套json字符串数组转换为Java对象…

十大常用web前端UI组件库,赶紧收藏

今天主要介绍web前端常用的UI库&#xff0c;这些网站基本都是背靠互联网大厂&#xff0c;值得web前端开发者收藏&#xff0c;当然还是要多多学习使用。 Vant 一款有赞出品轻量、可靠的的移动UI组件库&#xff0c;目前支持 Vue2、Vue3、React&#xff0c;微信和支付宝小程序&a…

Vue基础语法知识(自用,完整版)

Vue基础语法知识 1、想让Vue工作&#xff0c;就必须创建一个Vue实例&#xff0c;且要传入一个配置对象 2、root容器里的代码依然符合html规范&#xff0c;只不过混入了一些特殊的Vue语法 3、root容器中里的代码被称为【Vue模板】 4、Vue实例和容器是一一对应的 5、真实开发…

手机+PC电脑如何使用油猴插件和油猴脚本?

一、移动端使用油猴脚本 移动端可以不使用油猴插件&#xff0c;就可直接安装脚本&#xff08;需要浏览器支持&#xff09;&#xff0c;这样天然支持油猴脚本的移动浏览器还是很多&#xff0c;比如&#xff1a;书签地球、X浏览去、M浏览器等&#xff0c;但是各个浏览器的支持情…

前端Vue项目部署到服务器过程,以及踩坑记录

目录 需求 过程 踩坑 nginx服务器404错误的原因 404 Not Found nginx服务器500错误的原因 500 Internal Server Error nginx反向代理 需求 前后端完成项目后&#xff0c;需要部署到本地的服务器上。 第一次部署&#xff0c;以下是从0开始慢慢学着弄的详细过程 过程 …

大白话理解-微信小程序获取授权

学习目录 微信小程序前端直接获取授权微信为什么要授权微信怎么授权?微信登录微信小程序前端直接获取授权 微信为什么要授权 微信用户授权,才可以操作微信官方的某些接口。简单来说就是:微信定义了很多接口,然后他们认为有一部分是涉及到用户使用安全的,所以把这一部分划…