学了一段时间的react+ts,想着写一个项目练练手,由于初次写react+ts项目,有很多东西并不知道应该怎么写,再加上之前写vue项目的习惯,突然转换react有点不习惯,有很多在vue中的写法,并不知道是否在react中仍然可行。写项目之前先考虑了权限管理,第一次使用react-router v6 也不知道是否有更好的写法。这次就来简单分享一下我实现权限管理以及拦截器中遇到的一些问题。
权限管理
这次项目是有三种权限,分别是用户,商家以及管理员。这次写的权限管理是高级权限能访问低级权限的所有页面,但是低级权限不能访问高级权限的页面。
简单实现
// 在这里进行判断用户权限使用不同的页面
import React from "react";
import Merchant from "../pages/merchant";
import Admin from "../pages/admin";
import User from "../pages/user";
export default function Index() {
//这里使用power来判断是否登录,以及权限
let power = localStorage.getItem("power");
return (
<div>
{power ? (
power === "1" ? (
<Merchant></Merchant>
) : (
<Admin></Admin>
)
) : (
<User></User>
)}
</div>
);
}
我实现权限管理的方式就是根据不同的权限挂载不同的页面,在页面中实现挂载不同的路由。
之后看一下我在三种不同页面中的路由定义以及具体内容,即不需要显示的路由就不应该被注册
user路由表
// 引入路由
import Login from "../pages/user/login";
import Register from "../pages/user/register";
import NoPower from "../pages/error/noPower";
import NoPage from "../pages/error/noPage";
import { myRouteObj } from "../type";
//这里的myRouteObj是我自定义的一个类型用于拓展title实现页面标题的自动切换
export const userRoutes: myRouteObj[] = [
{
path: "/404",
element: <NoPage></NoPage>,
title: '404'
},
{
path: "/login",
element: <Login></Login>,
title: '登录'
},
{
path: "/register",
element: <Register></Register>,
title: '注册'
},
];
const routes = [
...userRoutes,
{
path: "*",
element: <NoPower></NoPower>,
title: '没有权限'
},
];
export default routes;
路由表中暴露了两个对象,分别是最"原始的对象"和使用的对象,这两个对象的区别就是是否有拦截器,为什么需要把这个给区分开,是因为我们的目的是高权限的用户能访问到低权限用户所能访问的页面,又由于我们是通过返回不同的页面来实现的权限管理(即使用过程中必须高权限的路由中包含低权限的路由)而且拦截器必须要放在最后,所以就分了两个来暴露,一个是供注册路由使用(有拦截器版本),另一个是由高级权限路由表合并使用
myRouteObj的内容
import { NonIndexRouteObject } from "react-router";
export interface myRouteObj extends NonIndexRouteObject {
title?: string;
}
user页面的入口文件
import React from "react";
import userRoutes from "../../router/user";
import { useMyRoutes } from "../../hooks/route";
export default function User() {
//这里的useMyRoutes是我自己封装的一个钩子函数
const element = useMyRoutes(userRoutes);
return <div>{element}</div>;
}
useMyRoutes钩子函数的具体内容
该钩子函数用于更换页面标题以及一级路由表中是否有该路由(没有的话会跳到404),这里为什么只判断了一级路由表是因为我个人感觉一级路由如果输入错误,其就应该跳转到404页面,而二级路由输入错误,应该重定向到二级路由正确的位置,这样我感觉对用户要更友好一点,所以我在二级路由中添加了重定向的功能,并且也实现了替换标题的功能,如果没有标题的话会返回一个默认值
import { useEffect } from "react";
import { useLocation, useNavigate, useRoutes } from "react-router";
import { myRouteObj } from "../type";
function flatDeep(children: any, findArr: any): string {
// 判断children数组中是否有想要的元素
let tempArr = children.map((item: any) => item.path);
let index = tempArr.indexOf(findArr[0]);
if (index !== -1) {
return findArr.length > 1
? flatDeep(children[index], findArr.splice(1, findArr.length))
: children[index].title;
? children[index].title
: "商城";
}
return "商城";
}
export function useMyRoutes(routes: myRouteObj[]) {
// 判断路由表中是否有对应路由,如果没有就返回到404
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
/*
这里有一个小问题,困扰了我半天,就是原本我获取路径是写在useEffect外边的,页面每次刷新就会显示成错误的标题,最后输出是因为报存的值没有更新,所以就改到useEffect函数里边了
*/
// 非空
const firstPathName = location.pathname
.split("/")
.filter((item) => item !== "");
const firstPath = routes.map((item) => item.path?.split("/")[1]);
let index = firstPath.indexOf(firstPathName[0]);
if (!index) {
navigate("/404");
return;
}
// 判断是否是首页
if (location.pathname === "/") {
document.title = "商城";
return;
}
// 判断是几级路由
if (firstPathName.length > 1) {
// 多级
document.title = flatDeep(
routes[index].children,
firstPathName.splice(1, firstPathName.length)
);
return;
}
// 一级
document.title = routes[index].title
? (routes[index].title as string)
: "商城";
}, [location, navigate, routes]);
return useRoutes(routes);
}
商家的路由表
import { Navigate } from "react-router";
import Merchant from "../pages/merchant";
import Commodity from "../pages/merchant/commodity";
import { userRoutes } from "./user";
import { getRoutesObj } from "../utils";
import { myRouteObj } from "../type";
const children: myRouteObj[] = [
{
path: "commodity",
element: <Commodity></Commodity>,
},
];
const primitiveObj = {
path: "/merchant/*",
element: <Merchant></Merchant>,
title: '商店后台'
};
// 有拦截器版本
export const [routes, defaultRoutes] = getRoutesObj(
userRoutes,
primitiveObj,
children,
{
path: "*",
element: <Navigate to="/merchant/commodity"></Navigate>,
}
);
getRoutesObj是我自定义的为二级路由添加重定向的工具函数,至于为什么封装这个函数,其原因就是商家和管理员都要使用,原本是想把重定向添加到最外层,但是这个由于react-router v6并不是严格匹配模式,所以即使匹配上路由也会尝试往下匹配更精确的路由比如/about和/about:id所以封装了该函数添加二级拦截器
import { myRouteObj } from "../type";
// 第一个参数是复用的数组对象,第二个参数是有拦截器的对象
/**
*生成路由表配置(加拦截器)
* @export
* @param {myRouteObj[]} reuseObj 复用的数组对象
* @param {myRouteObj} targetObj 目标对象
* @param {myRouteObj[]} childrenObj 子级路由
* @param {myRouteObj} globalinterceptorObj 全局拦截路由
* @param {myRouteObj} interceptor 可选拦截器对象(添加子级路由中)
*/
export function getRoutesObj(
reuseObj: myRouteObj[],
targetObj: myRouteObj,
childrenObj: myRouteObj[],
interceptorObj?: myRouteObj
) {
// 先复制一份children
const children = [...childrenObj];
// 判断是否有值
if (interceptorObj) {
children.push(interceptorObj);
}
return [
[
...reuseObj,
{
...targetObj,
children: children,
},
],
[
...reuseObj,
{
...targetObj,
children: [...childrenObj],
},
],
];
}
商家入口文件
import React, { Fragment } from "react";
import { Outlet } from "react-router";
import { routes } from "../../router/merchant";
import { useMyRoutes } from "../../hooks/route";
import { myRouteObj } from "../../type";
export default function Merchant() {
const element = useMyRoutes(routes as myRouteObj[]);
return (
<Fragment>
{element}
<Outlet></Outlet>
</Fragment>
);
}
管理员的和商家的差不多,这里就不再过多叙述了
总结
实现这个权限管理还是花了有一天多的时间,有很多东西没有这么写过,也不知道正规的是否应该这样写,不过现在还是先实现功能就行了,等之后随着不断的练习和学习,也能检验我这个方法是否是对的,第一次写react+TS的项目,仍有很多不足,如果哪位大佬有觉得不妥的地方,也欢迎指出,一同学习,共同进步。