前言
最近在撸一个后台管理项目,用的React18 + ts 路由用的v6.4版本,这也是我第一次在React中实现动态路由
思路
graph TD
用户登陆成功 --> 获取到当前用户的菜单 --> 根据菜单筛选出路由
实现代码
router/index.tsx 默认路由
import React from "react";
import { Navigate, RouteObject } from "react-router-dom";
// 路由懒加载
const Login = React.lazy(() => import(`../view/Login`));
const Main = React.lazy(() => import(`../view/Main`));
const routes: RouteObject[] = [
{
path: "/",
id: "root",
element: <Navigate to="/system/user" />,
},
{
path: "/login",
id: "login",
element: <Login />,
},
{
id: "main",
element: <Main />,
},
];
export default routes;
数据我是统一让Redux管理的,我用的是Redux Toolkit 这里就不介绍它了 感兴趣的可以去看看
store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import loginReducer, { loadLocalLogin } from "./modules/login";
// 创建一个 Redux
const store = configureStore({
reducer: {
login: loginReducer,
},
});
// 统一在这里初始化一些缓存的数据
export function setupStore() {
// 这里是缓存的菜单,程序加载会先调用这个
store.dispatch(loadLocalLogin());
}
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
store/modules/login.ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
// 这里统一加载缓存的一些数据
export const loadLocalLogin = createAsyncThunk(
"login/loadLocalLogin",
(_, { dispatch }) => {
const menus = localStorage.getItem("menus");
if (menus) {
dispatch(changeMenusAction(JSON.parse(menus)));
}
}
);
const loginSlice = createSlice({
name: "login",
initialState: {
menus: [],
},
reducers: {
changeMenusAction(state, { payload }) {
// 把数据存到redux里面,有点类似vuex
state.menus = payload;
localStorage.setItem("menus", JSON.stringify(payload));
},
},
});
export const { changeMenusAction } = loginSlice.actions;
export default loginSlice.reducer;
再来看看登陆页面 当我们点击登陆的时候就会跑通上面的代码了
import React, { memo, useState } from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { changeMenusAction } from "../../store/modules/login";
import { menus } from "./data";
// 菜单数据格式 menus
// [
// {
// type: 1,
// path: '/about'
// },
// {
// type: 2,
// children: [
// {
// type: 1,
// path: '/system/user'
// },
// {
// type: 1,
// path: '/system/role'
// },
// ]
// }
// ]
const Login = memo(() => {
const [isLogin, setIsLogin] = useState(false);
const dispatch = useDispatch();
const navigate = useNavigate();
function loginHandle() {
setIsLogin(!isLogin);
// 模拟请求登陆
setTimeout(() => {
// 这里调用的是刚刚上面redux导出的函数
dispatch(changeMenusAction(menus));
navigate("/system/user");
}, 2000);
}
return (
<div>
<h2>{!isLogin ? "Login" : "登录中。。。"}</h2>
<button onClick={loginHandle}>登录</button>
</div>
);
});
export default Login;
这里我们写一个自定义hook 我是不想把很多东西都赛在App组件里面
import { useEffect, useState } from "react";
import { shallowEqual, useSelector } from "react-redux";
import { RootState } from "../store";
import defaultRoutes from "../router";
import { handleMergeRoutes, mapMenusToRouter } from "../utils/mapMenus";
export function useLoadRouter() {
const [routes, setRoutes] = useState(defaultRoutes);
const { menus } = useSelector(
(state: RootState) => ({
menus: state.login.menus,
}),
shallowEqual
);
// useEffect监听的是redux/login里面的menus数据有没有改变
useEffect(() => {
// mapMenusToRouter方法是把菜单转成路由
const newRoutes = mapMenusToRouter(menus);
// handleMergeRoutes方法是把默认路由和新路由合并起来
const router = handleMergeRoutes(routes, newRoutes);
// 最后设置最新的路由
setRoutes(router);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [menus]);
return routes;
}
上面的自定义hook完成之后再来看看那俩个函数都做了什么吧
图001
import React from "react";
import { RouteObject } from "react-router-dom";
import { IMenus } from "../view/Login/data";
// 菜单转路由
export function mapMenusToRouter(userMenus: IMenus[]) {
const routes: RouteObject[] = [];
// 1.默认加载所有路由
const allRoutes: RouteObject[] = [];
// 这里加载的是 图001 router/main/* 的所有文件路径
const routeFiles = require.context(`../router/main`, true, /.tsx/);
routeFiles.keys().forEach((key) => {
// 根据路径导入路由
const route = require(`../router/main${key.split(".")[1]}`);
allRoutes.push(route.default);
});
// 2.根据menus来筛选路由
function filterRoute(menus: IMenus[]) {
for (const menuItem of menus) {
if (menuItem.type === 1) {
const route = allRoutes.find((item) => item.path === menuItem.path);
if (route) {
routes.push(route);
}
} else if (menuItem.type === 2) {
filterRoute(menuItem.children ?? []);
}
}
}
filterRoute(userMenus);
// 返回筛选完的路由
return routes;
}
// 合并路由
export function handleMergeRoutes(
defaultRoutes: RouteObject[],
routes: RouteObject[]
) {
// 拷贝原路由(坚持React数据不变的力量)
const newDefaultRoutes = deepCopyRoute<RouteObject[]>(defaultRoutes);
// 拿到main路由
const routeItem = newDefaultRoutes.find((item) => item.id === "main") ?? {};
// 添加新路由
routeItem.children = routes;
// 返回新路由
return newDefaultRoutes;
}
// 拷贝路由对象
function deepCopyRoute<T>(raw: T): T {
let copyData: any = Array.isArray(raw) ? [] : {};
for (const key in raw) {
const value: any = raw[key];
// 如果是普通类型或者react元素则不深拷贝
const condition = typeof value !== "object" || React.isValidElement(value);
if (condition) {
copyData[key] = value;
} else if (typeof value === "object") {
copyData[key] = deepCopyRoute(value);
}
}
return copyData;
}
App.jsx
import React, { memo } from "react";
import { useRoutes } from "react-router-dom";
import { useLoadRouter } from "./hooks/useRouter";
const App = memo(() => {
// 直接调用上面写好的hook即可,这样子就不会很乱啦
const routes = useLoadRouter();
return <>{useRoutes(routes)}</>;
});
export default App;
最后index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import store, { setupStore } from "./store";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
// 上面说了会调用这个加载缓存
setupStore();
root.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
);
结束
有更好的指教可以评论见哦~