位置: IT常识 - 正文

Umi4 从零开始实现动态路由、动态菜单(umi ts)

编辑:rootadmin
Umi4 从零开始实现动态路由、动态菜单 Umi4 从零开始实现动态路由、动态菜单🍕 前言🍔 前期准备📃 数据表🤗 Mock数据🔗 定义类型🎈 开始🎃 获取路由信息🧵 patchRoutes({ routes, routeComponents})📸 生成动态路由所需的数据formattedRoutePathroutePathcomponentPathfilePath🍖 生成动态路由数据及组件😋 完成✨ 踩坑🍕 前言

推荐整理分享Umi4 从零开始实现动态路由、动态菜单(umi ts),希望有所帮助,仅作参考,欢迎阅读内容。

文章相关热门搜索词:umi 教程,umi0,umi使用,umi0,umi 教程,umi0,umi 教程,umi 教程,内容如对您有帮助,希望把文章链接给更多的朋友!

近期在写 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 记录IDmenu_id 菜单的唯一IDparent_id 父级菜单的IDenable 是否启用菜单(后端或查询时进行过滤)name 路由名称、菜单名称、页面标题sort 菜单排序(后端或查询时进行排序)(xxxx 代表:一级菜单序号 子菜单序号)path 前端页面访问路径(同location.pathname)direct 是否为直接访问的菜单(即不存在子菜单和子路由,为顶级项目)created_at 记录创建时间🤗 Mock数据// ./mock/dynamicRoutes.tsexport 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.tsimport 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.tsimport type { DynamicRoutes } from '@/utils/dynamicRoutes/typing';import '@umijs/max/typings';declare global { interface Window { dynamicRoutes: DynamicRoutes.RouteRaw[]; }}🎈 开始🎃 获取路由信息// @/global.tsimport { 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.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)

Umi4 从零开始实现动态路由、动态菜单(umi ts)

动态更新路由需要直接修改由参数解构出的 routes和 routeComponents对象,使用 Object.assign(routes, newRoutes)将他们与新数据合并

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

以下三处需要使用DynamicRoutes.RouteRaw.path经过格式化后的路径:

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

要生成的路径:

formattedRoutePathroutePathcomponentPathfilePathformattedRoutePath// @/utils/dynamicRoutes/index.tsexport 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/UserroutePath// @/utils/dynamicRoutes/index.tsexport function generateRoutePath(path: string) { return path.toLowerCase();}

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

转换前:/System/User转换后:/system/usercomponentPath// @/utils/dynamicRoutes/index.tsexport 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/indexReact.lazy(() => import(`@/pages/${componentPath}`)) // 使用时filePath// @/utils/dynamicRoutes/index.tsexport 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-ignoreexport 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.tsimport 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-ignoreexport 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,导致组件路径错误
本文链接地址:https://www.jiuchutong.com/zhishi/283203.html 转载请保留说明!

上一篇:开机要按f1才能进入系统原因(开机要按f1才能启动)

下一篇:错误代码678是什么意思解决方法(报错代码678什么意思)

  • 小米12spro怎么取消按键震动(小米12怎么关机)

    小米12spro怎么取消按键震动(小米12怎么关机)

  • vivox70怎么应用加密(vivox7如何设置应用锁?)

    vivox70怎么应用加密(vivox7如何设置应用锁?)

  • excel如何去重复项(excel如何去重复项然后把重复项数值相加)

    excel如何去重复项(excel如何去重复项然后把重复项数值相加)

  • pps影音为什么没有了(pps影音为什么没有声音了)

    pps影音为什么没有了(pps影音为什么没有声音了)

  • 大数据技术可以分为哪几种类型(大数据技术可以在银行工作吗)

    大数据技术可以分为哪几种类型(大数据技术可以在银行工作吗)

  • 直播live是什么软件(live直播是什么词性)

    直播live是什么软件(live直播是什么词性)

  • 苹果id一般是什么格式(苹果id一般是什么组合icloud)

    苹果id一般是什么格式(苹果id一般是什么组合icloud)

  • led投影仪寿命是多久(led投影仪寿命是多少年)

    led投影仪寿命是多久(led投影仪寿命是多少年)

  • 没有开启拼小圈别人能看到我买的东西吗(没有开启拼小圈为什么会推送好友)

    没有开启拼小圈别人能看到我买的东西吗(没有开启拼小圈为什么会推送好友)

  • 华为手机拉黑后对方打电话会知道吗(华为手机拉黑后对方打电话什么提示)

    华为手机拉黑后对方打电话会知道吗(华为手机拉黑后对方打电话什么提示)

  • ppt批注怎么显示(ppt批注怎么设置)

    ppt批注怎么显示(ppt批注怎么设置)

  • mivideo文件夹可以删除吗(miguvideo是什么文件夹)

    mivideo文件夹可以删除吗(miguvideo是什么文件夹)

  • 开通黄钻隐身访问别人空间,别人看得到吗(开通黄钻隐身访问别人空间相册,别人看得到吗)

    开通黄钻隐身访问别人空间,别人看得到吗(开通黄钻隐身访问别人空间相册,别人看得到吗)

  • 信号弱的地方如何增强手机信号(信号弱的地方如何处理)

    信号弱的地方如何增强手机信号(信号弱的地方如何处理)

  • 淘宝直播二维码怎么生成(淘宝主播如何生成直播二维码)

    淘宝直播二维码怎么生成(淘宝主播如何生成直播二维码)

  • 手机视频源格式错误怎么解决(手机视频源格式错误怎么办)

    手机视频源格式错误怎么解决(手机视频源格式错误怎么办)

  • 红米note9pro上市时间(红米note9pro上市时间及价格)

    红米note9pro上市时间(红米note9pro上市时间及价格)

  • 微博注销了还能用原手机号注册吗(微博注销了还能搜到吗)

    微博注销了还能用原手机号注册吗(微博注销了还能搜到吗)

  • 麒麟990相当于电脑什么处理器(麒麟990相当于什么电脑cpu)

    麒麟990相当于电脑什么处理器(麒麟990相当于什么电脑cpu)

  • 数据线一定要原装的吗(数据线一定要原装的吗快充)

    数据线一定要原装的吗(数据线一定要原装的吗快充)

  • word编辑不了怎么办(word怎么编辑不了怎么回事)

    word编辑不了怎么办(word怎么编辑不了怎么回事)

  • mate30pro屏幕容易碎吗(Mate30pro屏幕容易坏)

    mate30pro屏幕容易碎吗(Mate30pro屏幕容易坏)

  • 手机qq密码忘记了怎么找回来(手机qq密码忘记了手机号也换了怎么办)

    手机qq密码忘记了怎么找回来(手机qq密码忘记了手机号也换了怎么办)

  • 微星主板显卡插哪个槽(微星主板显卡插槽)

    微星主板显卡插哪个槽(微星主板显卡插槽)

  • 探探共同联系人是指什么(探探共同联系人有什么用)

    探探共同联系人是指什么(探探共同联系人有什么用)

  • 苹果备忘录怎么画画(苹果备忘录怎么变成黑色)

    苹果备忘录怎么画画(苹果备忘录怎么变成黑色)

  • ipad定位服务在哪里(ipad定位服务在哪里打开)

    ipad定位服务在哪里(ipad定位服务在哪里打开)

  • 手机转接头使用方法(手机转接头使用图片方法)

    手机转接头使用方法(手机转接头使用图片方法)

  • 路由器2.4G WiFi和5G WiFi的区别是什么?(路由器2.4g和4g有什么区别)

    路由器2.4G WiFi和5G WiFi的区别是什么?(路由器2.4g和4g有什么区别)

  • 《花雕学AI》06:抢先体验ChatGPT的九个国内镜像站之试用与综合评测(花雕典故)

    《花雕学AI》06:抢先体验ChatGPT的九个国内镜像站之试用与综合评测(花雕典故)

  • 小规模纳税人企业所得税2023
  • 工会经费发放奖金需要代扣个税吗
  • 企业对企业分红要缴纳什么税免税分红
  • 资产负债表的期初余额是年初余额吗
  • 房地产开发企业预缴增值税
  • 资产负债表里的固定资产是原值还是净值
  • 处置子公司全部股权会计处理 企业会计准则
  • 预缴增值税是否适用一般计税方法
  • 小规模纳税人转一般纳税人
  • 增值税发票密码忘记了怎么办
  • 对外投资所得税计算公式
  • 个体办税务登记需要带什么
  • 高新企业职工教育经费扣除比例
  • 本月进项税额不够抵扣怎么办
  • 企业收到预收账款,能不能给购买方开具发票
  • 不是公司股东的投资者可以提前拿走投资款吗
  • 农产品收购发票上的买价含税吗
  • 报税没有报怎么办
  • 股票期权所得税率
  • 同一控制下企业合并入账价值
  • 小规模季报营业税怎么算
  • 公司与股东的往来款涉税
  • 融资租入固定资产的改建支出计入什么科目
  • 集团公司收取管理费是否可以税前列支
  • 环境保护税法是什么意思
  • 超出经营范围开发票
  • mac隔空投送文件位置
  • window休眠
  • 给员工买饮料
  • 免抵退税怎么做账
  • 山茶花的养殖方法盆栽
  • win10商店发生了错误请稍后再试
  • 嵌套if怎么用
  • 法定盈余公积金达到注册资本的多少时不再提取
  • 划转国有划拨土地流程
  • yii框架文档
  • 网上蛋糕商城jsp页面
  • matlab进行图像处理
  • 基于web的疫情防控方案
  • php怎么定义全局变量
  • php floatval
  • 建行对账单回签平啥意思
  • 年底应交增值税借方余额怎么处理
  • 资产负债表日后事项是什么意思?
  • 现金周期和经营周期的计算公式
  • 小微企业的资产负债率一般为多少
  • 未使用的固定资产
  • 修理费账务处理
  • 任意盈余公积金的用途
  • 企业所得税汇算表
  • 商品进销差价会计科目流程图模板
  • 增值税申报表中期初未缴税额指什么
  • 材料暂估入库需要什么原始凭证
  • 商场联营扣点的合作方式
  • 应收票据的计价应按
  • 企业发生的经济业务主要有哪些
  • 开发成本六大类
  • 发票开错了记账凭证如何做?
  • 冲红的发票抵扣联要放在凭证里吗
  • 取得存款利息收入需附
  • 汇票本票支票的联系
  • 应收账款余额等于什么?
  • 老生常谈啥意思
  • mysql57服务无法启动,找不到文件夹
  • 设置ubuntu
  • 虚拟机怎么安装exe文件
  • win10网卡驱动不正常连不上网怎么办
  • win8.1开机进入桌面
  • js立即执行函数几种写法
  • Cocos2d唯一死敌的崛起,OGEngine来了
  • cocos2d安装教程
  • unity的协程
  • unity3d2019安装步骤
  • unity3d移动代码
  • android 图形引擎
  • 税务通知书一般什么内容
  • 许昌市民之家有餐厅吗在几楼
  • 2023居民医保怎么交
  • 12366国税网上申报
  • 扬州退契税政策
  • 免责声明:网站部分图片文字素材来源于网络,如有侵权,请及时告知,我们会第一时间删除,谢谢! 邮箱:opceo@qq.com

    鄂ICP备2023003026号

    网站地图: 企业信息 工商信息 财税知识 网络常识 编程技术

    友情链接: 武汉网站建设