前言
react-router 更新到v6版本
应该有好一段时间了,但是v6自己也没真正去实践过,用过v5
版本的都知道如果配置路由守卫、拦截等或者像vue
那样的路由数组的话是很麻烦的,还要用到react-router-config
的这个库,但是升级到v6
版本后api
改动就变得很大了,比如说可以直接支持路由数组
的配置,history
跳转改成用useNavigate
、switch
改成用Routes
等,刚好最近也是想基于最新的react和react-router来搭建一个博客网站,特别是在配权限路由的时候花费了不少时间(可能是我比较菜的原因 😬),整体框架是基于vite
搭建的,数据存储用的是redux-toolkit
,请求用的是redux-toolkit
内置的query
模块(功能还是蛮强大的
),下面讲一下我是怎么一步步配置的(有什么错误欢迎大家指出来哈
)
路由配置
useRouters 的使用
useRoutes
是 React Router v6 中的一个 Hook,它的作用是动态地配置路由,它接收一个路由数组,并使用匹配到的路由来渲染相应的组件。
与传统的配置路由的方式不同,useRoutes
的优势在于它可以动态地配置路由,使得在应用的生命周期中更改路由变得更加容易。
我们在router
的文件夹里新建了一个index.tsx
(存放我们的路由数组)和AuthRouter
(路由权限控制。。。)的文件
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"]
,然后你根据auth
的user
访问的是user
的路由,写个过滤的方法将routes
数组过滤掉就行了,大概的思路就是这样
redux-toolkit 和 query配置
redux-toolkit
相比于传统的redux
主要的作用就是简化redux
代码冗长、简化操作等功能,它是将一些列功能集合在一个切片
中,更好的方便我们去管理我们的状态数据,而不用像传统的redux
又要分aciton
、reducer
等文件管理。
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;
里面的fetchBaseQuery
和InterceptorsResponse
这里我统一封装了一个函数去处理一个事请求前在每个api
中的header
加token
验证和响应后拦截的处理,具体看下面的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:
这里还有我自己用Go
搭建的后端框架,和这个前端是搭配一起的,后面的也会慢慢开源起来,敬请期待!