前言
从 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 组件创建之后,上述写法中 useRoutes
和 BrowserRouter
在同一个组件内,相当于在 BrowserRouter
还没有 render 的时候就执行了,因此会报错。
至此,我们就实现了根据权限组的不同构建不同的路由表了