react router6动态添加路由

3,789 阅读3分钟

20221213-164810.gif

前言

最近在撸一个后台管理项目,用的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 image.png

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>
);

结束

有更好的指教可以评论见哦~