Featured image of post react-router-domv6 动态路由

react-router-domv6 动态路由

使用 useRoutes 实现

前言

从 Vue 转 React 这么久,项目都没有用过动态路由,在新项目里遇到了多达 5 个身份的用户组,感觉不搞一个动态路由之后的项目会很难维护。前前后后踩了不少坑,遇到了很多逆天的 bug

正文

react-router-dom 更新到 v6 版本之后,我们获得了一个新的 hook useRoutes,从而原生地支持了路由表映射路由,不用再手写 Route 渲染了,今天就分享一下我的动态路由思路

首先,我们需要维护一张路由表,由于我们是用的 ts,所以自定义一个类型,从而在一张路由表里配置 meta 等属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { lazy, Suspense } from "react"

// 自定义懒加载函数
const lazyLoad = (factory: () => Promise<any>) => {
  const Module = lazy(factory)
  return (
    <Suspense fallback={<Loading />}>
      <Module />
    </Suspense>
  )
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
export interface IRouteObject {
  children?: IRouteObject[]
  element?: React.ReactNode
  index?: boolean
  path?: string
  meta?: {
    auth?: boolean
    role?: UserLevel
  }
}

export const Routes: IRouteObject[] = [
  {
    path: "",
    element: <Frame />,
    meta: {
      auth: true,
    },
    children: [
      {
        index: true,
        element: lazyLoad(() => import("@/pages/Home")),
      },
      {
        path: "cloud",
        element: lazyLoad(() => import("@/pages/Cloud")),
        meta: {
          role: UserLevel["ADMIN"]
        }
      },
    ],
  },
  {
    path: "/login",
    element: lazyLoad(() => import("@/pages/Login")),
  },
  {
    path: "*",
    element: <NotFound />,
  },
]

随后我们需要写一个 routeFilter 函数,用于根据原数据动态生成路由,我们可以在这里根据信息做一个路由守卫。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export const routeFilter = (
  routes: IRouteObject[],
  role: UserLevel = UserLevel["访客"]
): IRouteObject[] => {
  const newRoutes = [] as IRouteObject[]
  for (let route of routes) {
    if (route.meta?.auth && !getToken()) {
      continue
    }
    if (route.meta?.role) {
      // role 是数组,要求用户等级在数组中
      if (route.meta.role instanceof Array) {
        if (!route.meta.role.includes(role)) continue
      } // role 不是数组,要求用户等级大于等于role
      else {
        if (role < route.meta.role) continue
      }
    }
    const item = { ...route }
    // 如果路由有子路由,则递归过滤子路由
    if (route.children) {
      item.children = routeFilter(route.children, role)
    }
    newRoutes.push(item)
  }
  return newRoutes
}

这里有个细节,routeFilter 函数需要保证不能改变原数组,如果我们直接返回 routes.filter(xxxx) 的话,会导致后续即便权限升高也无法正常构建路由

然后我们再写一个自定义 hooks,把逻辑封装进去

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { useEffect, useState } from "react"
import { RouteObject } from "react-router-dom"

const useLoadRoutes = () => {
  const [routes, setRoutes] = useState(Routes as RouteObject[])
  const { level = 0 } = useRootStore((state) => state.userInfo)

  useEffect(() => {
    const newRoutes = routeFilter(Routes, level)
    setRoutes(newRoutes as RouteObject[])
  }, [level])

  return routes
}

export default useLoadRoutes

最后使用 useRoutes 注册路由

1
2
3
4
5
6
7
8
9
// src/router/index.tsx
import useLoadRoutes from "@/hooks/useLoadRoutes"
import { RouteObject, useRoutes } from "react-router-dom"

const Router: React.FC = () => {
  const routes = useLoadRoutes()
  return useRoutes(routes as RouteObject[])
}
export default Router

这里的 useRoutes 会自动构建出形同 <Routes><Route></Route></Routes> 的结构,因此无需在外层包裹 <Routes>

但是 useRoutes 只能在 Router 组件内部调用,所以我们需要在它外层包裹一层 BrowserRouter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// src/App.tsx
import Router from "@/router"
import { FC } from "react"
import { BrowserRouter } from "react-router-dom"

const App: FC = () => {
  return (
    <BrowserRouter>
      <Router />
    </BrowserRouter>
  )
}
export default App

注意,这里必须要在 Router 组件外包裹 useRoutes,像这样写也是不行的

1
2
3
4
const Router: React.FC = () => {
  const routes = useLoadRoutes()
  return <BrowserRouter>{useRoutes(routes as RouteObject[])}</BrowserRouter>
}

其原因在于

You should have a <BrowserRouter> (or any of the provided routers) higher up in the tree. The reason for this is that the <BrowserRouter> provides a history context which is needed at the time the routes are created using useRoutes(). Note that higher up means that it can’t be in the <App> itself, but at least in the component that renders it.

Form: reactjs - React Router V6 - Error: useRoutes() may be used only in the context of a component - Stack Overflow

意思是 useRoutes 的执行时机必须在 BrowserRouter 或者其他路由 Provider 组件创建之后,上述写法中 useRoutesBrowserRouter 在同一个组件内,相当于在 BrowserRouter 还没有 render 的时候就执行了,因此会报错。

至此,我们就实现了根据权限组的不同构建不同的路由表了

CC BY-NC-ND
最后更新于 Apr 07, 2023 01:12 +0800
Built with Hugo
主题 StackJimmy 设计