React 按需生成动态路由(非全量)

599 阅读4分钟

最近我在学习 React,用脚手架搭了个管理系统。在动态生成路由这块碰到了点问题,我在网上搜了很多文章资料,有些都是文章跟动态路由都没关系,看的头疼,放个demo,路由全量引入然后加载,这也叫动态生成路由,不太理解。反正得出结论,查了一堆没啥用(当然是我太菜了)。

我理解的动态路由生成是:在用户登录后,通过接口获取到后端返回的当前用户角色的菜单数据结构,然后前端根据这个数据结构动态生成路由,并异步地将其添加到路由配置中。而不是一次性将所有路由都加载到配置文件中。

我得到的答案:说的都是“全量加载”,即前端配置好所有路由,然后根据后端返回的当前角色权限来校验是否能访问。这虽然是一种方法,但我希望通过动态加载的方式,避免每次操作都修改前端配置文件。

React 中没有类似 Vue 的 addRoute 方法,因此我们需要自定义一个方法来动态加载路由。通过在用户登录后,调用接口获取到菜单数据,并根据这个数据动态生成路由,最后通过 window.location.replace 触发路由重新加载。

流程如下:

  • 管理员操作菜单配置模块
  • 后端保存菜单数据
  • 用户登录成功,前端调用接口获取当前角色的菜单数据
  • 前端根据菜单数据动态生成路由

初始化的时候是需要让后端添加一个菜单配置模块的,我这里是自己用的node搭的后端。

1. 用户登录和获取菜单数据

login.jsx:用户登录后,通过 API 获取用户信息和菜单数据,然后存储在 Redux 中。

const Login = () => {
  const dispatch = useDispatch();
  const [loading, setLoading] = useState(false);

  const onSubmit = async (data) => {
    const { account, password } = data;
    setLoading(true);

    // 调用登录接口
    const actionResult = await dispatch(loginAsync({ account, password: md5(password) }));

    if (actionResult.type === loginAsync.fulfilled.toString()) {
      // 获取用户信息
      const userResult = await userAPI.getUserInfo();
      if (userResult.status === 200) dispatch(setUserInfo(userResult.data));

      // 获取菜单数据
      const menusResult = await userAPI.getMenu({});
      if (menusResult.status === 200) {
        await dispatch(setMenus(menusResult.data)); // 存储菜单数据
        window.location.replace("/");  // 使用 replace 跳转来触发路由重新加载
      }
    }

    setLoading(false);
  };

  return <form onSubmit={onSubmit}> {/* 登录表单 */} </form>;
};

2. Redux 存储用户信息和菜单数据

通过 Redux 存储用户信息和菜单数据,并将菜单数据保存在 localStorage 中,以便刷新后继续使用。

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { session, storage } from "../utils/storage";
import { userAPI } from "@/http/api/user";

const initialState = {
  token: session.get("token") || "",
  userInfo: {},
  menus: JSON.parse(storage.get("menus")) || [],
};

const userReducer = createSlice({
  name: "user",
  initialState,
  reducers: {
    setUserInfo(state, { payload }) {
      state.userInfo = payload;
    },
    setMenus(state, { payload }) {
      state.menus = payload;
      storage.set("menus", JSON.stringify(payload)); // 存储菜单到本地
    },
    loginOut(state) {
      state.token = "";
      session.removeAll();
      storage.removeAll();
      window.location.replace("/login");
    },
  },
});

export const loginAsync = createAsyncThunk("user/login", async (loginData, thunkAPI) => {
  const { status, data } = await userAPI.login(loginData);
  return status === 200 ? data : thunkAPI.rejectWithValue(data);
});

export const { loginOut, setUserInfo, setMenus } = userReducer.actions;
export default userReducer.reducer;

3. 动态生成路由配置

在路由配置文件 router/index.js 中,根据后端返回的菜单数据生成路由配置,并通过 createBrowserRouter 创建路由。

import { createBrowserRouter, Navigate } from "react-router-dom";
import { lazy } from "react";
import store from "@/store";

// 递归生成路由
const generateRoutes = (menus = []) => {
  return menus.map((menu) => {
    const { componentPath, children, path } = menu;
    const Element = lazy(() => import(`@/views/${componentPath}`));  // 异步加载组件

    const route = {
      element: (!children || children.length <= 0) && <Element />,
      path,
    };

    if (children && children.length > 0) {
      route.children = generateRoutes(children);  // 递归生成子路由
    }

    return route;
  });
};

// 获取路由配置
const getRoutes = () => {
  const menus = store.getState().user.menus;  // 获取菜单数据
  if (menus && menus.length > 0) {
    const resultRoutes = generateRoutes(menus);
    const replaceUrl = resultRoutes[0].children?.[0]?.path || resultRoutes[0].path; // 获取重定向地址到第一个菜单
    return [
      {
        path: "/",
        element: <Navigate to={replaceUrl} replace />, // 重定向
      },
      ...resultRoutes,
    ];
  }
  return [];
};

// 路由配置
const routes = [
  {
    path: "/",
    element: <Layout />,
    children: getRoutes(),
  },
  {
    path: "/login",
    element: <Login />,
  },
  {
    path: "*",
    element: <NotFound />,
  },
];

export default createBrowserRouter(routes);

4. 在 App.js 中渲染路由

最后,在 App.js 中使用 RouterProvider 渲染路由配置。

import { RouterProvider } from "react-router-dom";
import router from "./router";

function App() {
  return (
    <div className="App">
      <RouterProvider router={router} />
    </div>
  );
}

export default App;

通过以上步骤,成功实现了根据后端数据动态生成路由。具体流程是:用户登录后,前端通过 API 获取当前角色的菜单数据,并动态生成路由配置。这样就能避免每次操作时都需要修改静态的路由配置文件,提升了开发效率和系统灵活性。

版本:

"react": "^18.3.1",

"react-redux": "^9.1.2",

"react-router-dom": "^6.22.2",