由于公司业务领域的变动,被迫由 Angular 转为了 React,(两者之间有关系吗?🤔)。虽说是赶鸭子上架,但其实上手写业务感觉还好,没有特别吃力。这篇笔记就记录一下基于 React 做权限管理。主要涉及到页面权限的鉴别,暂时不涉及按钮级别(其实是准备以按钮级别权限 + 基于资源的权限管理为内容再水一篇笔记)
完整代码:codesandbox.io/s/react-ts-…
路由守卫的设计
说白了路由守卫不就是在路由变更的时候执行一段函数,从而进行对页面的跳转。所以只要梳理清楚不同页面所需权限以及这些权限的优先级,即能完成路由守卫的编写。
坑爹的是之前做 Angular 或者 Vue 项目的时候,框架有现成的接口。例如 Angular 中的 canActivate 和 Vue 中的 beforeEach,但是 React 没有官方接口或者类。所以在看了一些 React 项目源码之后发现需要自己写一个组件充当路由守卫。
AuthRouter 组件的编写
// AuthRouter.tsx 文件
import { FC } from "react";
type IProps = {
children: JSX.Element;
};
/**
* 路由守卫组件
*/
const AuthRouter: FC<IProps> = ({ children }) => {
return children;
};
export default AuthRouter;
在 index.tsx 中引入并包裹入口页面,这里在哪个文件里面用不是重点,重点是要包裹入口页面。
// index.tsx 文件
import AuthRouter from "./router/AuthRouter";
// App 组件是我的入口页面,里面啥都没有,所有页面都将在 <App /> 组件内渲染
<AuthRouter>
<App />
</AuthRouter>
现在查看效果就会发现 Page 页面被原封不动的渲染出来了。(别杠我的代码风格,杠就是你赢。我直接从 codesandbox 上粘贴出来的)
贴一下路由配置
这里的配置相对比较简单,不涉及动态路由,后面会单独说动态路由的问题。
/**
* 路由配置
*/
import { Suspense, lazy, LazyExoticComponent, ReactNode } from "react";
import { RouteObject, Navigate, useRoutes } from "react-router-dom";
/**
* 自定义懒加载组件,添加 fallback
* @param {LazyExoticComponent} Element 懒加载组件
* @returns {ReactNode} react 组件
*/
function lazyLoad(Element: LazyExoticComponent<any>): ReactNode {
return (
<Suspense fallback={<div>加载中...</div>}>
<Element />
</Suspense>
);
}
export interface RouteProps extends RouteObject {
children?: RouteProps[];
meta?: {
title?: string;
auth?: boolean;
roles?: string[];
};
}
// 路由配置
export const routes: RouteProps[] = [
{
path: "/",
element: <Navigate to="login" replace />
},
{
path: "/login",
element: lazyLoad(lazy(() => import("../pages/Login"))),
meta: {
title: "登录页",
auth: false
}
},
{
path: "/dashboard",
element: lazyLoad(lazy(() => import("../pages/Dashboard"))),
meta: {
title: "数据看板",
auth: true,
roles: ["Admin"]
}
},
{
path: "*",
element: <Navigate to="/login" />
}
];
// 这里用了 ReactRouter V6 的 hook
const Router = () => useRoutes(routes);
export default Router;
给路由守卫添加逻辑
我们先梳理一下守卫的逻辑哈。
- 判断目标路由是否需要权限
- 如果不需要权限,直接放行
- 如果需要权限,判断当前用户是否登录
- 若未登录,跳转至登录页面(或指定页面)
- 已登录,判断用户是否有该页面权限
- 若无权限,跳转至指定页面
- 若有权限,直接放行
接下来就是把逻辑翻译成代码语言。
判断目标路由是否需要权限
想要进行判断至少要知道两件事情:
- 目标路由是啥:可以从
react-router-dom中的useLocation获取pathname - 目标路由的 auth 属性是啥:这个可以从路由配置数组中知道
两者都知道了我们只需要写个查找函数,在路由配置数组中查找满足 path === pathname 的一项就可以知道目标路由是否需要权限。查找函数我就不写了,大家的路由配置表结构可能不一,请自力更生。
// AuthRouter.tsx 文件
import { useLocation } from "react-router-dom";
/**
* 查找目标路由
* @param {RouteProps} routes 路由配置数组
* @returns {string} pathname 目标路由 path
*/
const getTargetPath: RouteProps = (routes: RouteProps[], pathname: string) => {
// do something
};
const AuthRouter: FC<IPtops> = ({ children }) => {
// ....
const { pathname } = useLocation();
const target = getTargetPath(routes, pathname);
// 目标路由无需权限,直接放行
if (!target?.auth) return children;
// 需要权限,检查是否已登录
const token = getToken();
if (!token) return <Navigate to="/login" />;
// 需要权限,检查是否有该权限
const roles = intersection(user.roles, target.roles) // intersection 函数为两数组交集,请自立更生
if (!roles || roles.length <= 0) return <Navigate to="/login" />;
// ....
};
到此为止其实整个路由守卫就算是写完了,剩下的就是根据实际情况进行一些改造。
但是这种方案有一个对我来说不太友好的地方,getTargetPath 的查找方式不够灵活,我们在配置路由的时候可能会有动态路由,例如 /list/1/detail/11,但是查找又是完全匹配查找,这就导致我明明有该路由,但是找不到。所以我就在想有没有啥方法能直接把目标路由返给我,最好是官方就有相关的 API,翻了一下文档,可是让我找到了一个接口能解决这个事情:matchRoutes
改进版
改造的重点其实就是替换原有的 getTargetPath 函数。
// AuthRouter.tsx 文件
const location = useLocation();
const match = matchRoutes(routes, location);
const target = match[match.length - 1].route;
最后
请各位大佬不吝赐教!🙏🙏🙏