React Router、And、Redux动态菜单和动态路由

news/2024/10/10 20:18:55

前言

动态菜单和动态路由的逻辑

在登录完成之后,用useEffect监听dispatch把菜单和路由的数据初始化,渲染菜单,redux将路由的静态资源修改。

数据结构

后端数据符合前端需要的数据结构即可,mock后端接口返回数据

import Mock from 'mockjs';Mock.mock('/api', 'get', {code: 200,data:{menuLists: [{id: '@id',name: 'Layout',path: '/home/homes',icon: 'ShopOutlined',label: '首页',},{id: '@id',name: 'Publish',path: '/publish',label: '文章管理',icon: 'DesktopOutlined',children: [{id: '@id',name: 'Article',path: '/publish/article',label: '创建文章',icon: 'SignatureOutlined',children: []},{id: '@id',name: 'Mark',path: '/publish/mark',label: '标注文章',icon: 'FormOutlined',children: [{id: '@id',name: 'High',icon: 'AreaChartOutlined',path: '/publish/mark/high',label: '高亮文章',children: []}]}]},],routerLists:[{id: '@id',name: 'Layout',path: '/publish',children: [{id: '@id',name: 'Article',path: '/publish/article',children: []},{id: '@id',name: 'Mark',path: '/publish/mark',children: [{id: '@id',name: 'High',path: '/publish/mark/high',children: []}]}]},]}})

动态菜单

数据结构

antd的menu组件中有items属性用于显示菜单内容。需要按照数据结构才能显示

interface Menu {label: string,icon: string,key: stringchildren?: Menu[]
}

这里如果菜单不是多级路由把Children属性去掉,否则路由会成为一个父级路由

初始化菜单
const addMenuList = (datas) => {const menus: Menu[] = [];datas.forEach(items => {let menu: Menu = {icon: items.icon,label: items.label,key: items.path,}if (items.children && items.children.length != 0) {// 递归处理子菜单const child: Menu[] = addMenuList(items.children);// 只有在子菜单非空时,才给父菜单项添加 children 属性if (child.length > 0) {menu.children = child;}}const flag = menus.find(it => it.key === menu.key)if (!flag) {menus.push(menu)}})return menus
}
初始化图标

将icon图标进行动态添加,因为存储的为不可序列化对象,控制台会报错但不影响使用,图标不会用来序列化可以忽略

import React from "react";
import * as Icons from '@ant-design/icons'
import {Menu} from "@/store/routers/MyRouterStore.tsx";const iconList: any = Icons// 修改方法,避免直接修改原始数据
export  function addIconToMenu(menuData: Menu[]) {// 递归处理每个菜单项return menuData.map((item: any) => {// const item = {...item}; // 创建新的对象,避免修改原始数据// 如果菜单项有 icon 属性,则创建对应的 React 元素if (item.icon) {const IconComponent = iconList[item.icon]; // 获取对应的图标组件item.icon = React.createElement(IconComponent); // 创建 React 元素}// 如果菜单项有 children 属性,则递归处理子菜单项if (item.children) {item.children = addIconToMenu(item.children); // 递归处理子菜单项}return item; // 返回更新后的菜单项}); 
}
渲染菜单公共组件

const location = useLocation()
const navigate = useNavigate();// 获取菜单
const items = useSelector(state => state.route.menuList)//跳转路由
const menuPath = (route) => {navigate(route.key)    
}//也可以递归进行渲染菜单
// const renderMenuItems = (data) => {//     return data.map(item => {//         if (item.children && item.children.length > 0) {//             // 如果有子菜单,递归渲染子菜单//             return (//                 <SubMenu key={item.key} title={item.label} icon={item.icon}>//                     {renderMenuItems(item.children)}//                 </SubMenu>//             );//         } else {//             // 没有子菜单的情况//             return (//                 <Menu.Item key={item.key} icon={item.icon}>//                     {item.label}//                 </Menu.Item>//             );//         }//     });// };//将要去的路径高亮
const lightHeight = location.pathname
return(
<Layout style={{minHeight: '100vh'}}><Sider collapsible><div className="demo-logo-vertical"/><Menu theme="dark" selectedKeys={[lightHeight]} mode="inline" onClick={menuPath} items={items}>{/*{renderMenuItems(items)}*/}</Menu></Sider><Layout><Header style={{padding: 0, background: colorBgContainer}}></Header><Content style={{margin: '0 16px'}}><Breadcrumb style={{margin: '16px 0'}}></Breadcrumb><Outlet></Outlet></Content><Footer style={{textAlign: 'center'}}>Ant Design ©{new Date().getFullYear()} Created by Ant UED</Footer></Layout></Layout>)

动态路由

静态路由(准备好公用的静态路由和拼接路由方法)

将后端返回数据的路径拼成页面路由,不为公共样式的可能是子菜单需要拼接

export const load = (name: string, path: string) => {let Page: React.LazyExoticComponent<React.ComponentType<any>>if (name !== 'Layout') {// 将路径字符串按 '/' 分割成数组const parts = path.split('/');// 遍历数组,对每个路径部分进行首字母大写处理const capitalizedParts = parts.map(part => {if (part.length > 0) {// 将单词的首字母大写,再加上剩余部分return part.charAt(0).toUpperCase() + part.slice(1);} else {return part; // 对于空字符串部分保持不变}});// 将处理后的路径部分拼接回字符串,使用 '/' 连接const capitalizedPath = capitalizedParts.join('/');console.log('capitalizedPath', capitalizedPath)Page = lazy(() => import(`../pages${capitalizedPath}`))} else {Page = lazy(() => import(`../pages/${name}`))}return (<Suspense fallback={'等待中'}><AuthRoute><Page></Page></AuthRoute></Suspense>)
}

静态路由

const LazyHome=lazy(()=>import(`@/pages/Home`))
export let routerLists = [{path: '/login',element:<Suspense fallback={'等待中'}><Login/></Suspense>},{path: '/',element: <Navigate to={'/home/homes'}/>,},{path: '/home',element: <AuthRoute><Lay/></AuthRoute>,children: [{path: 'homes',element: <Suspense fallback={'等待中'}><LazyHome /></Suspense>,}]},
]
动态路由初始化(递归方法与菜单递归相似)
interface Router {path: stringchildren?: Router[],element: any
}const addRouterList = (routers) => {const routerLis: Router[] = []routers.forEach(items => {let route: Router = {path: items.path,element: load(items.name, items.path),}if (items.children && items.children.length != 0) {// 递归处理子菜单const child: Router[] = addRouterList(items.children);// 只有在子菜单非空时,才给父菜单项添加 children 属性if (child.length > 0) {route.children = child;}}const flag = routerLis.find(it => it.path === route.path)if (!flag) {routerLis.push(route)}})return routerLis
}

判断固定静态路由长度避免重复添加路由

const info = (routers) => {if (routerLists.length==3){routers.forEach(item=>{routerLists.push(item)})  }
}

在菜单公共组件中监听dispatch的变化,修改动态路由

const dispatch = useDispatch()useEffect(() => {dispatch(getRouterList())}, [dispatch])
将动态路由渲染出来

这里的两个监听,1防止第一次渲染吧路由清空,之后当redux的路由变化时修改路由列表2浏览器刷新时防止白屏没路由,判断是否只有静态路由

function Routes() {const router = useSelector(state => state.route.routerList)const dispatch= useDispatch()//监听路由进行修改useEffect(() => {if (router.length>0){setRouterList(router)}}, [router]);//防止白屏没路由useEffect(() => {if (routerLists.length==3){dispatch(getRouterList())}}, []);console.log('routerLists',routerLists)const element = useRoutes(routerLists)return <>{element}</>
}
ReactDOM.createRoot(document.getElementById('root')!).render(<Provider store={store}><BrowserRouter><Routes/></BrowserRouter></Provider>
)

用到的工具

判断是否存在Token组件

包裹组件后,当组件不存在token会被强制跳转登录页面

import {getToken} from "@/utils";
import {Navigate} from "react-router-dom";function getToken() {return sessionStorage.getItem("token")
}function setToken(token: string) {sessionStorage.setItem("token", token)
}function removeToken() {sessionStorage.removeItem("token")
}//组件前判断是否有token
export default function AuthRoute({children})
{const token= getToken()if (token){return <>{children}</>}else {return  <Navigate to={'/login'} replace/>}
}

遇到的问题

  1. 白屏问题
  • 在路由文件使用懒加载必须使用Suspense标签进行包裹,否则在登录进来会白屏。如果不使用标签包裹则引用组件
  • 在路由树中必须要监听一下路由列表是否有动态加载过,否则刷新页面后就会白屏
  1. 修改路由
  • 修改路由列表前,要判断是否存在动态新增的路由,避免重复修改。也避免清空路由列表

完整代码

src
├─ App.tsx
├─ apis
│    ├─ article.ts
│    ├─ mock.ts
│    └─ user.ts
├─ components
│    └─ AuthRoute.tsx
├─ hooks
│    └─ useChannel.ts
├─ index.scss
├─ main.tsx
├─ pages
│    ├─ Home
│    │    ├─ component
│    │    │    └─ Barschar.tsx
│    │    └─ index.tsx
│    ├─ Layout
│    │    ├─ index.scss
│    │    └─ index.tsx
│    ├─ Login
│    │    ├─ index.tsx
│    │    └─ login.scss
│    └─ Publish
│           ├─ Article
│           │    ├─ index.scss
│           │    └─ index.tsx
│           ├─ Mark
│           │    ├─ High
│           │    │    └─ index.tsx
│           │    └─ index.tsx
│           ├─ index.tsx
│           └─ inter.ts
├─ router
│    ├─ index.tsx
│    └─ routerList.tsx
├─ store
│    ├─ index.tsx
│    ├─ routers
│    │    └─ MyRouterStore.tsx
│    └─ users
│           └─ index.ts
├─ utils
│    ├─ TokenUtil.ts
│    ├─ icon.ts
│    ├─ index.ts
│    └─ request.ts
AuthRoute.tsx
import {getToken} from "@/utils";
import {Navigate} from "react-router-dom";//组件前判断是否有token
export default function AuthRoute({children})
{const token= getToken()if (token){return <>{children}</>}else {return  <Navigate to={'/login'} replace/>}
}
main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.scss'
import {BrowserRouter} from "react-router-dom";
import {Provider} from "react-redux";
import store from "@/store";
import { ConfigProvider } from 'antd';
import zh_CN from 'antd/locale/zh_CN';
import '@/apis/mock.ts'
import Routes from "@/router";ReactDOM.createRoot(document.getElementById('root')!).render(<ConfigProvider locale={zh_CN}><Provider store={store}><BrowserRouter><Routes/></BrowserRouter></Provider></ConfigProvider>
)
Layout(index.tsx)
import React, {useEffect, useState} from 'react';import {MenuProps, Popover} from 'antd';
import './index.scss'
import {Breadcrumb, Layout as Lay, Menu, theme} from 'antd';
import {Outlet, useLocation, useNavigate} from "react-router-dom";
import {useDispatch, useSelector} from "react-redux";
import {fetchUserInfo} from "@/store/users";
import {removeToken} from "@/utils";
import {getRouterList} from "@/store/routers/MyRouterStore.tsx";const {Header, Content, Footer, Sider} = Lay;const Layout: React.FC = () => {const dispatch = useDispatch()const [collapsed, setCollapsed] = useState(false);const location = useLocation()const navigate = useNavigate();const [open, setOpen] = useState(false)const [title, setTitle] = useState([])const {token: {colorBgContainer, borderRadiusLG},} = theme.useToken();const items = useSelector(state => state.route.menuList)//跳转路由const menuPath = (route) => {navigate(route.key)}// const renderMenuItems = (data) => {//     return data.map(item => {//         if (item.children && item.children.length > 0) {//             // 如果有子菜单,递归渲染子菜单//             return (//                 <SubMenu key={item.key} title={item.label} icon={item.icon}>//                     {renderMenuItems(item.children)}//                 </SubMenu>//             );//         } else {//             // 没有子菜单的情况//             return (//                 <Menu.Item key={item.key} icon={item.icon}>//                     {item.label}//                 </Menu.Item>//             );//         }//     });// };useEffect(() => {dispatch(fetchUserInfo())dispatch(getRouterList())}, [dispatch])//获取个人信息const userName = useSelector(state => state.user.userInfo.name)//将要去的路径const lightHeight = location.pathnamereturn (<Lay style={{minHeight: '100vh'}}><Sider collapsible><div className="demo-logo-vertical"/><Menu theme="dark" selectedKeys={[lightHeight]} mode="inline" onClick={menuPath} items={items}>{/*{renderMenuItems(items)}*/}</Menu></Sider><Lay><Header style={{padding: 0, background: colorBgContainer}}></Header><Content style={{margin: '0 16px'}}><Outlet></Outlet></Content><Footer style={{textAlign: 'center'}}>Ant Design ©{new Date().getFullYear()} Created by Ant UED</Footer></Lay></Lay>);
};export default Layout;
Login(index.tsx)
import React from 'react';
import {SafetyOutlined, UserOutlined} from '@ant-design/icons';
import {Button, Form, Input, message} from 'antd';
import './login.scss'
import {useDispatch} from "react-redux";
import {fetchLogin} from "@/store/users";
import {useNavigate} from "react-router-dom";const Login: React.FC = () => {const dispatch = useDispatch()const navigate = useNavigate()const onFinish = async (values: any) => {//进行登录await dispatch(fetchLogin(values))// 完成跳转navigate('/')message.success("登录成功")};return (<div className="login"><Formname="normal_login"className="login-form"initialValues={{remember: true}}onFinish={onFinish}validateTrigger={'onBlur'}><div className="in"><span>登录</span></div><Form.Itemname="mobile"rules={[{required: true, message: '请输入账号!'}]}><Input style={{width: '400px'}} size={"large"} prefix={<UserOutlined/>} placeholder="账号"/></Form.Item><Form.Itemname="code"rules={[{required: true, message: '请输入密码!'}]}><Inputstyle={{width: '400px'}}size={"large"}prefix={<SafetyOutlined/>}type="password"placeholder="密码"/></Form.Item><Form.Item><Button size={"large"} type="primary" htmlType="submit" className="login-form-button">登录</Button></Form.Item></Form></div>);
};export default Login;
router
index.tsx
import React, {useEffect} from "react"
import {useRoutes} from "react-router-dom"
import {routerLists, setRouterList} from "@/router/routerList.tsx";
import {useDispatch, useSelector} from "react-redux";
import {getRouterList} from "@/store/routers/MyRouterStore.tsx";function Routes() {const router = useSelector(state => state.route.routerList)const dispatch= useDispatch()useEffect(() => {if (router.length>0){console.log('router',router)setRouterList(router)}}, [router]);useEffect(() => {if (routerLists.length==3){dispatch(getRouterList())}}, []);console.log('routerLists',routerLists)const element = useRoutes(routerLists)return <>{element}</>
}export default Routes
routerList.tsx
import {Navigate} from "react-router-dom";
import AuthRoute from "@/components/AuthRoute.tsx";
import React, {lazy, Suspense} from "react";
import Login from "@/pages/Login";
import Layout from "@/pages/Layout";export const load = (name: string, path: string) => {let Page: React.LazyExoticComponent<React.ComponentType<any>>if (name !== 'Layout') {// 将路径字符串按 '/' 分割成数组const parts = path.split('/');// 遍历数组,对每个路径部分进行首字母大写处理const capitalizedParts = parts.map(part => {if (part.length > 0) {// 将单词的首字母大写,再加上剩余部分return part.charAt(0).toUpperCase() + part.slice(1);} else {return part; // 对于空字符串部分保持不变}});// 将处理后的路径部分拼接回字符串,使用 '/' 连接const capitalizedPath = capitalizedParts.join('/');console.log('capitalizedPath', capitalizedPath)Page = lazy(() => import(`../pages${capitalizedPath}`))} else {Page = lazy(() => import(`../pages/${name}`))}return (<Suspense fallback={'等待中'}><AuthRoute><Page></Page></AuthRoute></Suspense>)
}const LazyHome=lazy(()=>import(`@/pages/Home`))
export let routerLists = [{path: '/login',element:<Suspense fallback={'等待中'}><Login/></Suspense>},{path: '/',element: <Navigate to={'/home/homes'}/>,},{path: '/home',// lazy:()=>import("@/pages/Home")element: <AuthRoute><Layout/></AuthRoute>,children: [{path: 'homes',element:<Suspense fallback={'等待中'}><LazyHome /></Suspense>}]},
]export const setRouterList = (data) => {routerLists = data
}
store
MyRouterStore.tsx
import {createSlice} from "@reduxjs/toolkit";
import {user} from "@/apis/user.ts";import {addIconToMenu} from "@/utils";
import {load, routerLists} from "@/router/routerList.tsx";export interface Menu {label: string,icon: string,key: stringchildren?: Menu[]
}interface Router {path: stringchildren?: Router[],element: any
}const useStore = createSlice({name: 'route',initialState: {routerList: [] as Router[],            //动态路由数组menuList: [] as Menu[]              //菜单数组},reducers: {setRouterList(state, action) {state.routerList = action.payload},setMenuList(state, action) {state.menuList = action.payload}}
})const {setRouterList, setMenuList} = useStore.actionsconst useRouterReducer = useStore.reducerconst getRouterList = () => {return async (dispatch) => {const data = await user.routerList()const menus: Menu[] = addMenuList(data.data.menuLists)addIconToMenu(menus)const routers = addRouterList(data.data.routerLists)info(routers)dispatch(setMenuList(menus))dispatch(setRouterList(routerLists))}
}const addMenuList = (datas) => {const menus: Menu[] = [];datas.forEach(items => {let menu: Menu = {icon: items.icon,label: items.label,key: items.path,}if (items.children && items.children.length != 0) {// 递归处理子菜单const child: Menu[] = addMenuList(items.children);// 只有在子菜单非空时,才给父菜单项添加 children 属性if (child.length > 0) {menu.children = child;}}const flag = menus.find(it => it.key === menu.key)if (!flag) {menus.push(menu)}})return menus
}const addRouterList = (routers) => {const routerLis: Router[] = []routers.forEach(items => {let route: Router = {path: items.path,element: load(items.name, items.path),}if (items.children && items.children.length != 0) {// 递归处理子菜单const child: Router[] = addRouterList(items.children);// 只有在子菜单非空时,才给父菜单项添加 children 属性if (child.length > 0) {route.children = child;}}const flag = routerLis.find(it => it.path === route.path)if (!flag) {routerLis.push(route)}})return routerLis
}const info = (routers) => {if (routerLists.length == 3) {routers.forEach(item => {routerLists.push(item)})}}export {setRouterList,setMenuList,getRouterList
}
export default useRouterReducer
index.ts
import {configureStore} from "@reduxjs/toolkit";
import userReducer from "@/store/users";
import useRouterReducer from "@/store/routers/MyRouterStore.tsx";export  default configureStore({reducer: {user: userReducer,route:useRouterReducer},
})
utils
icon.ts
import React from "react";
import * as Icons from '@ant-design/icons'
import {Menu} from "@/store/routers/MyRouterStore.tsx";const iconList: any = Icons// 修改方法,避免直接修改原始数据
export  function addIconToMenu(menuData: Menu[]) {// 递归处理每个菜单项return menuData.map((item: any) => {// const item = {...item}; // 创建新的对象,避免修改原始数据// 如果菜单项有 icon 属性,则创建对应的 React 元素if (item.icon) {const IconComponent = iconList[item.icon]; // 获取对应的图标组件item.icon = React.createElement(IconComponent); // 创建 React 元素}// 如果菜单项有 children 属性,则递归处理子菜单项if (item.children) {item.children = addIconToMenu(item.children); // 递归处理子菜单项}return item; // 返回更新后的菜单项});
}

运行截图

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

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

相关文章

Mysql数据库备份及恢复

mysqldump -uroot -p******** test > test.sql备份test数据库到当前目录 mysqldump -uroot -pAbc123*** -t test > test1.sql也就是备份test数据库中的插入数据,如图所示:这样的备份不能在新的数据库中恢复,只能在原库恢复. mysqldump -uroot -pAbc123*** -d test >…

WPF 高仿360功能界面

WPF 高仿360功能界面,如图:XAML TabControl 控件<TabControl Style="{StaticResource TabControlStyle}"><TabItem Header="资源共享" Style="{StaticResource TabItemStyle}"><TabItem.Background><ImageBrush ImageSou…

软件设计师:操作系统知识

操作系统层次进程管理 顺序执行真题标记并发执行

软考备考

p5,https://www.bilibili.com/video/BV1Qc411G7fB?p=6&vd_source=1a563cd2b3f3fdeb2a16cbbf18022d2f第一章 计算机组成原理与体系结构基础知识(6) 信息化世界是由计算机/手机通过计算机网络与其他的计算机/手机连接的,其中,计算机/手机由三部分组成,从底层到上层分别…

单调队列:从一道题说起_2023CCPC广东省赛B.基站建设

今天遇到一道题: 给定长度为n的数组a,a[i]表示在第i点建立基站的开销。 同时给出m个区间[li,ri],要满足给定的m个区间内都至少有一个基站,求最小的开销。正解是单调队列优化dp,那么什么是单调队列?我们先看另外一道题:显然最小值和最大值是互相独立的,我们可以先考虑最…

Raven-1-WordPress-python命令提权

靶机下载地址:https://www.vulnhub.com/entry/raven-1,256/ 修改hosts文件DescriptionRaven is a Beginner/Intermediate boot2root machine. There are four flags to find and two intended ways of getting root. Built with VMware and tested on Virtual Box. Set up to …

Raven-2-WordPress-UDF提权

0x00 什么是UDF UDF 全称为:User Defined Function,意为用户自定义函数;用户可以添加自定义的新函数到Mysql中,以达到功能的扩充,调用方式与一般系统自带的函数相同,例如 contact(),user(),version()等函数。 udf 文件后缀一般为 dll,由C、C++编写。 0x01 UDF在渗透中…