React Router、Antd、Redux动态菜单和动态路由
1.React Router、Antd、Redux动态菜单和动态路由
前言
动态菜单和动态路由的逻辑
在登录完成之后,用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: string children?: 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: string children?: 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/> } }
遇到的问题
- 白屏问题
- 在路由文件使用懒加载必须使用Suspense标签进行包裹,否则在登录进来会白屏。如果不使用标签包裹则引用组件
- 在路由树中必须要监听一下路由列表是否有动态加载过,否则刷新页面后就会白屏
- 修改路由
- 修改路由列表前,要判断是否存在动态新增的路由,避免重复修改。也避免清空路由列表
完整代码
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.pathname return ( <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"> <Form name="normal_login" className="login-form" initialValues={{remember: true}} onFinish={onFinish} validateTrigger={'onBlur'} > <div className="in"> <span>登录</span> </div> <Form.Item name="mobile" rules={[{required: true, message: '请输入账号!'}]} > <Input style={{width: '400px'}} size={"large"} prefix={<UserOutlined/>} placeholder="账号"/> </Form.Item> <Form.Item name="code" rules={[{required: true, message: '请输入密码!'}]} > <Input style={{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: string children?: Menu[] } interface Router { path: string children?: 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.actions const useRouterReducer = useStore.reducer const 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; // 返回更新后的菜单项 }); }
运行截图
本文作者:Kang_kin
本文链接:https://www.cnblogs.com/kangkin/p/18173072
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2023-05-05 python异步爬虫