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

最近在工作中碰到了一些管理系统的开发需求,其中就包括了整个管理系统从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提供的强大属性,在比较简短的代码里就实现了一套完整的权限路由体系。

相关推荐
奋斗吧程序媛1 分钟前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿11 分钟前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748256562 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@2 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺4 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
F-2H6 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
gqkmiss6 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
m0_748247559 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255029 小时前
前端常用算法集合
前端·算法
真的很上进9 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html