如何通过react-router-dom V6.4 中的loader优雅的实现菜单权限和登陆拦截功能

5,974 阅读4分钟

项目地址

简单看下效果

1.gif
我们之前要做路由权限的时候我们一般怎么做呢? 在入口文件useEffect中调用用户信息接口, 然后把用户信息放到全局中供其他地方使用, 然后在写一个高阶组件,在组件内部拿到用户信息和当前的路由的权限字段去匹配,有就渲染,没有就返回403。

上面的做法会有什么不好的地方呢?上面的做法是已经渲染了页面,然后在获取用户信息,然后在通过高阶组件去判断是否渲染页面,获取用户信息是异步的,我们可能会出现,开始用户进入了某一个页面(用户信息接口比较慢),然后当用户信息接口返回用户信息时,发现该用户没有那个页面的权限 然后展示403。进入了 => 调取用户信息... => 在调转403。显然当用户信息获取比较慢时候不太友好。

如果我们能够在页面加载前就拿到用户的信息,然后在渲染的时候拿到渲染前就拿到的信息来做逻辑判断是否有权限展示,是不是就不会出现 开始有权限进入页面 =》 获取用户信息 =》 发现没权限 跳转403 这个问题了

官网地址

我们的功能都是基于Route中的loader属性来实现的

文档关于loader的介绍是: Each route can define a "loader" function to provide data to the route element before it renders.
翻译过来是: 每个路由都可以定义一个“加载器”函数,以便在渲染之前向路由元素提供数据, 关键字在渲染之前!!!

基于以上解释我思考了几个问题

  • 1 该函数是在渲染路由前触发,那我们是不是可以去调用获取用户信息接口?
  • 2 在loader里面调用用户信息接口后如何在页面内部获取呢?
  • 3 如果调用接口提示登陆过期, 那我是不是可以重定向到登陆页?
  • 4 如果调用接口成功, 我又能在页面中拿到返回的数据那是不是可以结合高阶组件实现路由权限

开搞开搞

  • 开搞前我们得先回答上面的问题
  • redirt方法可以用于重定向
  • useRouteLoaderData(id)可以用于获取loader返回的数据
  • useRouteError 可以拿到路由的错误信息

App.tsx

import { RouterProvider } from 'react-router-dom';
import { routes } from './routers';

const App = () => {
  return <RouterProvider router={routes} />
}

export default App

routers/index.tsx(核心代码)

import { lazy, Suspense } from 'react'
import { createBrowserRouter, Navigate, redirect } from 'react-router-dom'
import type { RouteObject } from 'react-router-dom'
import ErrorBoundary from '../components/ErrorBoundary'

// 不需要懒加载的页面组件
import Layout from '../pages/layout'
import Permission from '../components/Permission'
import NoFind from '../pages/noFind'

// 需要懒加载的页面组件
const Home = lazy(() => import('../pages/home'))
const List = lazy(() => import('../pages/list'))
const Detail = lazy(() => import('../pages/detail'))
const Login = lazy(() => import('../pages/login'))

/**
 * @param Component 懒加载的组件
 * @param code 用于判断权限的字段(你可以自己定)
 * @returns 
 */
const LazyLoad = (Component: React.LazyExoticComponent<() => JSX.Element>, code?: string) => {
   return (
      <Permission code={code}>
         <Suspense fallback={<div>loading...</div>}>
            <Component />
         </Suspense>
      </Permission>
   )
}

export interface UserInfo {
   name: string;
   age: number;
   permissionRoutes: string[];
   code: number;
}
/**
 * @description 模拟请求用户信息
 * @returns 
 */
export const getUserInfo = (): Promise<UserInfo> => {
   return new Promise(resolve => {
      setTimeout(() => {
         resolve({
            name: 'jianjian',
            age: 12,
            permissionRoutes: ['home', 'list'],
            code: 0
         })
      }, 1000);
   })
}


/**
 * @description 这个loader函数会在路由渲染前触发,所以可以用来做路由权限控制和登陆重定向
 * @description (取代请求拦截器中的登陆重定向)
 * @description 这个loader函数返回值可以在页面中通过 useRouteLoaderData(id)或者useLoaderData获取 
 */
const rootLoader = async () => {
   console.log('页面加载前请求用户信息');
   // 这里用假的接口模拟下
   const { permissionRoutes, name, age, code } = await getUserInfo();
   // 假设20001代表登陆过期
   if(code === 20001) {
      redirect('/login')
   }
   return {
      name,
      age,
      permissionRoutes
   }
}

const routerConfig: RouteObject[] = [
   {
      path: '/',
      element: <Navigate to='/home' />
   },
   {
      path: '/',
      id: 'root',
      errorElement: <ErrorBoundary />,
      element: <Layout />,
      loader: rootLoader,
      children: [
         {
            path: '/home',
            element: LazyLoad(Home, 'home')
         },
         {
            path: '/list',
            element: LazyLoad(List, 'list')
         },
         {
            path: '/detail',
            element: LazyLoad(Detail, 'detail')
         }
      ]
   },
   {
      path: '/login',
      element: LazyLoad(Login)
   },
   {
      path: '*',
      element: <NoFind />
   }
]

export const routes = createBrowserRouter(routerConfig)

components/Permission.tsx(权限组件)

import { FC, PropsWithChildren } from 'react'
import { useRouteLoaderData } from 'react-router-dom'
import type { UserInfo } from '../../routers'

interface Iprops {
   code?: string
}

const Permission: FC<PropsWithChildren<Iprops>> = (props) => {
   // 这个root是我们在前面路由中定义了 id: 'root'
   const loaderData = useRouteLoaderData('root') as UserInfo
   const { children, code } = props
   if(!code || loaderData?.permissionRoutes?.includes(code)) {
      return <>{children}</>
   }
   return <div>403...</div>
}

export default Permission

components/ErrorBoundary

import { useRouteError } from "react-router-dom";

const ErrorBoundary = () => {
   const err = useRouteError() as any;
   return <div>
      <p>
         出错啦~
      </p>
      <p>
         错误信息: {err.message}
      </p>
   </div>
}

export default ErrorBoundary;