likes
comments
collection
share

react-router v6 路由守卫最佳实践!

作者站长头像
站长
· 阅读数 18

前言

react-router 更新到v6版本应该有好一段时间了,但是v6自己也没真正去实践过,用过v5版本的都知道如果配置路由守卫、拦截等或者像vue那样的路由数组的话是很麻烦的,还要用到react-router-config的这个库,但是升级到v6版本后api改动就变得很大了,比如说可以直接支持路由数组的配置,history跳转改成用useNavigateswitch改成用Routes等,刚好最近也是想基于最新的react和react-router来搭建一个博客网站,特别是在配权限路由的时候花费了不少时间(可能是我比较菜的原因 😬),整体框架是基于vite搭建的,数据存储用的是redux-toolkit,请求用的是redux-toolkit内置的query模块(功能还是蛮强大的),下面讲一下我是怎么一步步配置的(有什么错误欢迎大家指出来哈)

路由配置

useRouters 的使用

useRoutes 是 React Router v6 中的一个 Hook,它的作用是动态地配置路由,它接收一个路由数组,并使用匹配到的路由来渲染相应的组件。

与传统的配置路由的方式不同,useRoutes 的优势在于它可以动态地配置路由,使得在应用的生命周期中更改路由变得更加容易。

我们在router的文件夹里新建了一个index.tsx(存放我们的路由数组)和AuthRouter(路由权限控制。。。)的文件

react-router v6 路由守卫最佳实践!

index.tsx

import { Suspense, lazy, ReactNode } from "react";
import { Outlet, Navigate, useRoutes, Route } from "react-router-dom";

import Layout from "../components/Layout";
import LoginPage from "../login";

import NotFound from "../components/404";
const Home = lazy(() => import("../home"));
const About = lazy(() => import("../about"));
const LinkPage = lazy(() => import("../link"));
const MdPage = lazy(() => import("../md"));
// const Layout = lazy(() => import("../components/Layout"));

const LayoutComponent = ({ children }: any) => {
  return (
    <Suspense fallback={""}>
      <Layout />
    </Suspense>
  );
};

export interface RouteConfig {
  path: string;
  element: React.ReactNode;
  auth: boolean;
  children?: RouteConfig[];
  redirect?:string
}

export const routers = [
  { path: "/login", element: <LoginPage />, auth: false },
  {
    path: "/",
    element: <Layout />,
    auth: true,
    children: [
      { path: "/home", element: <Home />, auth: true },
      { path: "/about", element: <About />, auth: true },
      { path: "/auth", element: <About />, auth: true },
      { path: "/link", element: <LinkPage />, auth: true },
      { path: "/md/:id", element: <MdPage />, auth: true },
      { path: "*", element: <NotFound />, auth: true },
    ],
  },
];

主要是看我们的AuthRouter

import { message } from "antd";
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { matchRoutes, useLocation, useNavigate } from "react-router-dom";
import { routers } from "./index";

const AuthRoute = ({ children, auth }: any) => {
  const navigate = useNavigate();
  const token = localStorage.getItem("blogToken") || "";
  const loginState = useSelector((state: any) => state.public.loginState);
  const mathchs = matchRoutes(routers, location);

  const isExist = mathchs?.some((item) => item.pathname == location.pathname);

  useEffect(() => {
    if (token == "" && auth) {
      message.error("token 过期,请重新登录!");
      navigate("/login");
    }
    // 这里判断条件是:token 存在并且是匹配到路由并且是已经登录的状态
    if (token && isExist && loginState == "login") {
      // 如果你已经登录了,但是你通过浏览器里直接访问login的话不允许直接跳转到login路由,必须通过logout来控制退出登录或者是token过期返回登录界面
      if (location.pathname == "/" || location.pathname == "/login") {
        navigate("/home");
      } else {
        // 如果是其他路由就跳到其他的路由
        navigate(location.pathname);
      }
    }
  }, [token, location.pathname]);

  return children;
};
export default AuthRoute;

然后在我们的App.tsx组件里专门写了个方法处理渲染路由

import { ReactNode, useCallback, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
  Route,
  Routes,
} from "react-router-dom";
import { RouteConfig, routers } from "./router";
import AuthRoute from "./router/AuthRoute";

const App = () => {

  const loginState = useSelector((state: any) => state.public.loginState);

  // 处理我们的routers
  const RouteAuthFun = (
    (routeList: RouteConfig[]) => {
      return routeList.map(
        (item: {
          path: string;
          auth: boolean;
          element: ReactNode;
          children?: any;
        }) => {
          return (
            <Route
              path={item.path}
              element={
                <AuthRoute auth={item.auth} key={item.path}>
                  {item.element}
                </AuthRoute>
              }
              key={item.path}
            >
              {/* 递归调用,因为可能存在多级的路由 */}
              {item?.children && RouteAuthFun(item.children)}
            </Route>
          );
        }
      );
    }
  );
  return <Routes>{RouteAuthFun(routers)}</Routes>;
};
export default App;

这里要注意一点的是必须要用Routes来包裹我们Route组件,这里是react-router v6强制要求的,不包裹的话就会报错

这里的router就大概配置到这里,如果要配置一些更复杂的路由权限的话也可以在这个基础上来配置,比如说我这里配置是通过auth是true或者false来判断,你也可以将auth写成数组的模式,通过里面的数组权限来过滤掉路由来访问不同的路由就可以了,比如说auth=["user","admin"],然后你根据authuser访问的是user的路由,写个过滤的方法将routes数组过滤掉就行了,大概的思路就是这样

redux-toolkit 和 query配置

redux-toolkit相比于传统的redux主要的作用就是简化redux代码冗长、简化操作等功能,它是将一些列功能集合在一个切片中,更好的方便我们去管理我们的状态数据,而不用像传统的redux又要分acitonreducer等文件管理。

redux-toolkit 中的 query 功能是一种从 Redux store 中查询数据的方式。它使用记忆化查询来提高性能,并可以通过简单的语法从 store 中读取数据。这对于组件在不执行重新渲染的情况下获取数据,并在 store 中的数据发生更改时重新渲染,非常有用。

下面我以loginApi.ts为例,里面存放着两个请求获取验证码登录的请求

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react";
import { GET_CAPTCHA, LOGIN } from "../api/api";
import { baseUrl, InterceptorsResponse } from "../api/baseQuery";
import { message } from "antd";

// 登录接口
const loginApi = createApi({
  reducerPath: "loginApi", // Api的标识,不能和其他的Api或reducer重复
  baseQuery: fetchBaseQuery({
    baseUrl,
  }),
  endpoints(build) {
    return {
      // 登录
      login: build.mutation({
        query(args) {
          return {
            url: LOGIN,
            params: args,
          };
        },
      }),
      // 获取验证码
      getCaptcha: build.query({
        query() {
          return GET_CAPTCHA;
        },
        // transformResponse 用来转换响应数据的格式
        transformResponse(res: any) {
          // console.log(res, "用来转换响应数据的格式....");
          return InterceptorsResponse(res);
        },
        // keepUnusedDataFor: 60, // 设置数据缓存的时间,单位秒 默认60s
      }),
    };
  }, // endpoints 用来指定Api中的各种功能,是一个方法,需要一个对象作为返回值
});

// Api对象创建后,对象中会根据各种方法自动的生成对应的钩子函数
// 通过这些钩子函数,可以来向服务器发送请求
// 钩子函数的命名规则 fetchLogin --> useFetchLoginQuery
export const { useLoginMutation, useGetCaptchaQuery } = loginApi;

export default loginApi;

里面的fetchBaseQueryInterceptorsResponse这里我统一封装了一个函数去处理一个事请求前在每个api中的headertoken验证和响应后拦截的处理,具体看下面的baseQuery的代码 baseQuery.ts

import { fetchBaseQuery } from "@reduxjs/toolkit/dist/query";
import { message } from "antd";

export const baseUrl = "http://localhost:8080";

type ResponseData = {
  code: number;
  data: any;
  message: string;
};

export const baseQuery = fetchBaseQuery({
  baseUrl,
  timeout: 1000,
  // 对所有的请求进行预处理
  prepareHeaders: (headers) => {
    const token = localStorage.getItem("blogToken");
    if (token) {
      headers.set("Authorization", `Bearer ${token}`);
    }
    return headers;
  },
});

// 统一响应拦截器
export const InterceptorsResponse = (res: any) => {
  try {
    const msg = res.message;
    switch (res.code) {
      case 200:
        return res;
      case 401:
        message.error(msg);
        localStorage.clear();
        window.location.href = "/login";
        return Promise.reject("error");
      case 500:
        message.error(msg);
        return Promise.reject(new Error(msg));
      default:
        message.error(msg);
        return Promise.reject("error");
    }
  } catch (error: any) {
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    } else if (message.includes("timeout")) {
      message = "系统接口请求超时";
    } else if (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    return Promise.reject(error);
  }
};

然后看我们的store.ts最终配置

import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import reducer from "./reducer";
import mdApi from "./mdApi";
import loginApi from "./loginApi";

const persistConfig = {
  key: "root",
  storage,
  // 配置持久化存在白名单
  whitelist: ["public"],
};

const sliceReducer = combineReducers({
  [mdApi.reducerPath]: mdApi.reducer,
  [loginApi.reducerPath]: loginApi.reducer,
  ...reducer,
});

const persistedReducer = persistReducer(persistConfig, sliceReducer);

const store = configureStore({
  reducer: persistedReducer,
  // middleware: (middle) => middle().concat([ loginApi.middleware, homeApi.middleware ])
  middleware: (middle) =>
    middle().concat([mdApi.middleware, loginApi.middleware]),
});

const persistor = persistStore(store);

export { store, persistor };

这里要注意的是:如果你是有用到query模块配置的话,必须要在middleware中间件配置这里配置上你的配置,如果是普通的reducer是不需要配置到这里的

我们在login登录界面调用我们loginApi.ts时这样调用

import React, { useEffect, useState } from "react";
import { Form, Input, Button, message, Checkbox } from "antd";
import {
  UserOutlined,
  LockOutlined,
  EyeInvisibleOutlined,
  EyeTwoTone,
} from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import styles from "./login.module.css";
import "./square.css";
import { useGetCaptchaQuery, useLoginMutation } from "../store/loginApi";
import { setLogin, setUserInfo } from "../store/reducer/pubilcSlice";
import { useDispatch } from "react-redux";

const LoginPage: React.FC<{}> = () => {
  const navigate = useNavigate();
  const dispatch = useDispatch();

  const {
    // 数据
    data: captcha,
    // 刷新方法
    refetch: refetchCaptcha,
    // 判断是否出错
    isError,
  } = useGetCaptchaQuery(null, {
    // 响应前对数据进行处理
    selectFromResult: (res) => {
      const newRes = { ...res };
      if (newRes.data?.code == 200) {
        newRes.data = {
          ...newRes.data,
          captcha_img: `data:image/png;base64,${newRes?.data?.data?.captcha_img}`,
        };
      }
      return newRes;
    },
  });

  const [fetchLogin, { isLoading }] = useLoginMutation();

  useEffect(() => {
    if (isError) {
      message.error("后端接口连接异常!");
    }
  }, [isError]);

  // const handleVerfiCaptcha = (rule: any, value: string, callback: any) => {
  //   if (
  //     value &&
  //     value.toLocaleUpperCase() !== captcha?.captcha_id.toLocaleUpperCase()
  //   ) {
  //     callback(new Error("Verification code error"));
  //   } else {
  //     callback();
  //   }
  // };

  const onFinish = async (values: any) => {
    try {
      const res: any = await fetchLogin(values).unwrap();
      if (res?.code == 200) {
        message.success(res?.message);
        const token = res?.data?.token;
        localStorage.setItem("blogToken", token);
        dispatch(setLogin("login"));
        dispatch(setUserInfo(res?.data?.user));
        navigate("/home");
      } else {
        message.error(res?.message);
        // 重新刷新验证码
        setTimeout(() => {
          refetchCaptcha();
        }, 1000);
      }
    } catch (error) {
      console.log(error);
    }
  };

  const onRefreshCatch = () => {
    refetchCaptcha();
  };

  return (
    <div className={styles.container}>
      <div className={styles.loginContent}>
        <div className={styles.loginLeft}></div>
        <div className={styles.loginRight}>
          <div className={styles.logTit}>Blog管理系统</div>
          <div className={styles.loginForm}>
            <Form
              name="normal_login"
              className="login-form"
              size="large"
              initialValues={{ remember: true }}
              onFinish={onFinish}
            >
              <Form.Item
                name="account"
                initialValue={"admin"}
                rules={[
                  {
                    required: true,
                    message: "Please input your Account!",
                  },
                ]}
              >
                <Input
                  prefix={<UserOutlined className="site-form-item-icon" />}
                  placeholder="userCount:admin"
                />
              </Form.Item>
              <Form.Item
                name="password"
                initialValue={"admin123"}
                rules={[
                  {
                    required: true,
                    message: "Please input your Password!",
                  },
                ]}
              >
                <Input.Password
                  prefix={<LockOutlined className="site-form-item-icon" />}
                  type="password"
                  placeholder="passWord:123456"
                  iconRender={(visible) =>
                    visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />
                  }
                />
              </Form.Item>
              <div className={styles.captchaContent}>
                <Form.Item
                  name="code"
                  rules={[
                    {
                      required: true,
                      message: "Please input your captcha!",
                    },
                    // { validator: handleVerfiCaptcha },
                  ]}
                >
                  <Input placeholder="captcha" />
                </Form.Item>
                <Form.Item>
                  {/* <div
                    dangerouslySetInnerHTML={{
                      __html: captcha?.captcha_img,
                    }}
                    onClick={onRefreshCatch}
                  ></div> */}
                  <img
                    src={captcha?.captcha_img}
                    style={{ width: "200px", height: "40" }}
                    alt=""
                    onClick={onRefreshCatch}
                  />
                </Form.Item>
              </div>
              <Form.Item labelCol={{ flex: "flex" }}>
                <Form.Item
                  name="remember"
                  valuePropName="checked"
                  noStyle
                  labelCol={{ sm: 6 }}
                >
                  <Checkbox>记住我</Checkbox>
                </Form.Item>

                <a className={styles.loginForgot} href="">
                  忘记密码
                </a>
              </Form.Item>

              <Form.Item>
                <Button
                  type="primary"
                  htmlType="submit"
                  loading={isLoading}
                  style={{ width: "100%" }}
                >
                  Login
                </Button>
              </Form.Item>
            </Form>
          </div>
        </div>
      </div>

      <div className="square">
        <ul>
          <li></li>
          <li></li>
          <li></li>
          <li></li>
          <li></li>
        </ul>
      </div>
      <div className="circle">
        <ul>
          <li></li>
          <li></li>
          <li></li>
          <li></li>
          <li></li>
        </ul>
      </div>
    </div>
  );
};

export default LoginPage;

最后

我们最后的示例就到此结束(还没完善的),这个案例后面也会继续慢慢完善的,希望最后也能帮到大家,也欢迎大家能提出issue,感谢🙏,也麻烦大家给个star

完整案例请看这里:demo

ui:

react-router v6 路由守卫最佳实践!

react-router v6 路由守卫最佳实践!

这里还有我自己用Go搭建的后端框架,和这个前端是搭配一起的,后面的也会慢慢开源起来,敬请期待!