前端的权限路由到底该咋写

最近在工作中碰到了一些管理系统的开发需求,其中就包括了整个管理系统从0到1的项目搭建工作。显然,无论你的管理系统面向的是什么类型的客户,或者需要实现什么类型的功能,你都应该在初始项目搭建时,清晰的知道,权限管理功能是最为基础也是最为重要的功能之一。

而今天,就让我们来谈谈,在权限管理中扮演着极为重要角色的一员,权限路由管理。

前言

RBAC

在开始具体的代码实现之前,我们先来好好的回顾一下到底什么是权限管理,以及如何进行权限管理。

通俗的来讲,当不同的用户登录系统的时候,即使是同一个系统,由于身份的不同,他们所能看到的页面和所能进行的操作都是不尽相同的,而这些区别则是所谓的权限,当然,权限本身更多时候是管理者所需要操心的事情。但是,怎么对这些用户以及这些用户的权限进行管理,则是开发者需要操心的事情。

在如今绝大部分的管理系统中,都采用了著名的RBAC权限控制模型来进行权限功能的管理,即:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。

很容易可以想到,在用户和权限中间添加了一层角色,大幅度增了权限管理的安全性和效率性。

实现思路

回到正题,权限路由实则就是让服务端来告诉前端,当前这个用户拥有哪些页面的访问权限。换句话来说,当用户访问我们的系统时,他的角色就决定了他能看到哪些页面。

大概的流程为:

从上述的流程图可以得知,前端在路由管理这块貌似跟RBAC没有任何关系。事实上的确如此,因为RBAC的角色权限模型通常维护在服务端,在用户通过登录态调用接口的时候,服务端就已经根据其用户信息,来得到其对应的角色,最后再告诉前端他的具体权限了,前端自然而然就不用操心这些事情了。

代码实现

让我们进入到具体的代码实现,以React技术栈为例子(Vue基本也大同小异,甚至利用VueRouter的addRoutes会更为简单),前端的路由库基本都是通过React-Router来进行管理。

路由定义

正常在开发的时候,我们会在routes.tsx中去定义我们全部的页面路由以及访问路径:

routes.tsx 复制代码
import {
  createBrowserRouter,
} from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: (
      <div>
        <h1>Hello World</h1>
      </div>
    ),
  },
  {
    path: "about",
    element: <div>About</div>,
  },
]);

export default router;

然后在页面的根组件中进行引入:

index.ts 复制代码
import * as React from "react";
import { createRoot } from "react-dom/client";
import {
  RouterProvider,
} from "react-router-dom";
import router from './routes'

createRoot(document.getElementById("root")).render(
  <RouterProvider router={router} />
);

但是,在动态路由的场景下,我们在开发时并不清楚哪些页面会展现,也不清楚页面与页面之间的具体层级结构。换句话来说,所有的页面结构全部由服务端下发。

假设,服务端返回的数据结构为:

vbnet 复制代码
const mockRes = [
    {
       // 页面的唯一Key
       key: 'Home',
       // 页面url
       url: '/home',
       chidrens: [
           key: 'User',
           url: '/user'
       ]
    }
]

此时,我们期待用户只能看到/home/home/user两个页面所对应的组件。

在我们重新组织路由相关的代码结构之前,我们先用一个Store来管理需要储存的共享状态(这里使用的是Zustand进行状态管理 juejin.cn/post/727416..., 当然Redux或者useReducer也都可以 ):

store/index.ts 复制代码
/**
 *  通用状态
 *  @ loginStatus 是否已登录
 *  @ permissionRoute  用户路由权限信息
 *  @ action 状态更新操作 
 */
interface CommonStoreProps {
  loginStatus: boolean;
  permissionRoute: RouteObject[];
  action: {
    updatePermissionRoute:  (routes: RouteObject[]) => void;
    updateLoginStatus:  (status: boolean) => void;
  }
}
export useRoutesStore = create<RoutesStoreProps>()((set) => ({
  loginStatus: false,
  permissionRoute: [],
  action: {
    updateLoginStatus: (status) => set(state => {
      return { loginStatus: status }
    }),
    updatePermissionRoute: (routes) => set(state => ({ permissionRoute: routes}))
  }
}))
export const getLoginStatus = () => useCommonStore(state => state.loginStatus);
export const getPermissionRoute = () => useCommonStore(state => state.permissionRoute);
export const getCommonAction = () => useCommonStore(state => state.action)

然后,我们新增路由的映射文件map.tsx,在里面根据服务端返回的key定义好每个页面的映射关系,同时维护好一些不需要权限的公共页面(例如登录页等),以及兜底的404页面。

map.tsx 复制代码
import React, { lazy } from "react";
/**
 * 动态路由
 */
export const asyncRoutes = {
  Home: lazy(() => import("@/pages/Home")),
  User: lazy(() => import("@/pages/User")),
};

/**
 * 公开路由
 */
export commonRoutes = {
   {
    path: "/login",
    Component: lazy(() => import("@/pages/login")),
  },
}


/**
 * 404页面
 */
 export const notFoundRoutes = {
  path: "*",
  Component: lazy(() => import("@/pages/notFound")),
};

最后,我们将routes.tsx改为一个hooks,实现一个工厂函数,在页面初始化的时候默认返回公开路由,当服务端返回权限列表之后,动态转换成React-Router所需要的路由结构,最终呈现页面。

routes.tsx 复制代码
import { useState, useEffect } from "react";
import { getAction, getPermissionRoute, getLoginStatus } from '@/store'
import { commonRoutes, notFoundRoutes } from './map'
import { formatPermissionRoutes } from './utils';

const useRouter = () => {
  // 接口的loading状态
  const [loading, setLoading] = useState<boolean>(false)
  // 更新store权限路由的方法
  const { updatePermissionRoute } = getCommonAction()
  // 订阅当前权限路由,权限路由更新时,重新生成新的全局路由
  const permissionRoute = getCommonPermissionRoute()
  const loginStatus = getCommonLoginStatus()  
  
  const init = async () => {
       setLoading(true);
       const permissionRoutes = await fetchRoutes();
       // 利用递归的工厂函数(后续会说),将服务端的路由结构转为`React-Router`的结构
       const finallRoutes = formatPermissionRoutes(permissionRoutes);
       // 更新store里的路由信息,方便其他组件使。,注意,需要接口返回才能把404的兜底路由更新进去,否则一开始就会显示404页面
       updatePermissionRoute([
        ...routes,
        notFoundRoutes
      ]);
  }
   /**
   * 根据默认路由和动态路由,生成路由组件
   */
  const Router = () => useRoutes([...permissionRoute, ...commonRoutes]);
  
  
  /**
   * 当用户登录态发生改变时,重新调用接口
   */
  useEffect(() => {
    if(loginStatus) {
      initRoutes()
    } else {
      // 判断是否处在公开路由下,否则跳转到登录页
      if(!withCommonRoute()) {
        jumoToLogin()
      }
    }
  }, [loginStatus])
}

页面呈现

在大部分的管理系统开发时,主要的页面结构都会以侧边栏和右侧内容区域为基础样式,这里我利用antd的Layout组件实现了基本的主体框架,利用React-Router的Outlet组件,我们的路由就会呈现在Content区域。

ts 复制代码
import React, { FC } from "react";
import { Outlet } from "react-router-dom";
import { Layout } from "antd";

const { Content } = Layout;
const BasicLayout: FC = () => {
  return (
    <Layout>
        {/* 侧边栏 */}
        <Sider/>
         {/* 主要内容 */}
        <Content>
          <Outlet></Outlet>
        </Content>
     </Layout>
   )
}

应该怎么组织我们的工厂函数呢?其实很简单,一个递归就搞定了。

ini 复制代码
import { asyncRoutes, commonRoutes } from "../routes";
import BasicLayout from "@/layout";

export const formatPermissionRoutes = (routes: ResponseRoutesType[]): RouteObject[] => {
  if (!routes.length) {
    return [];
  }
  return [{
    element: <BasicLayout />,
    path: "/",
    children:[ 
      ...recursion(routes)
    ],
  }];
};

export const recursion = (
  routes: ResponseRoutesType[],
  formatRoutesList: RouteObject[] = []
): RouteObject[] => {
  if (!routes || !routes.length) {
    return [];
  }

  routes.forEach((item) => {
    formatRoutesList.push({
      path: item.url,
      Component: asyncRoutes[item.key],
      children: recursionRoutes(item.chidrens),
    });
  });
  return formatRoutesList;
};

最后,只需要利用我们的Hooks,在接口返回后展现我们的入口组件即可。

App.tsx 复制代码
import { Suspense } from 'react'
import { BrowserRouter } from 'react-router-dom'
import useRoutes from './routes'

function App(){
  const { loading, Router } = useRoutes()
    return (
        <Suspense>
          {loading ? (
            loading...
          ) : (
            <BrowserRouter>
              <Router />
            </BrowserRouter>
          )}
        </Suspense>
    )
}

大功告成!

总结

通过上述代码,我们利用React和React-Router提供的强大属性,在比较简短的代码里就实现了一套完整的权限路由体系。

相关推荐
y先森24 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy24 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891127 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端