- 2022-03-02 更新:
修复项目打包后可能无法加载路由的问题;升级插件至1.0.0版本,该版本使用方式上有差别。 - 2022-02-22 更新:
重构代码组织,插件化处理(传送门)
实现的功能:
- 全局路由统一管理,支持便捷配置路由重定向、路由懒加载、自定义meta字段等。
- 全局路由拦截,支持读取路由meta配置,支持拦截跳转其他路由等。
- 同时也支持嵌套路由、动态路由参数等官方路由原生支持的配置方式。
下面分享具体方案。
一、react-router v6
"react": "^17.0.2",
"react-router-dom": "^6.2.1",
"prop-types": "^15.8.1",
v6版本目前网上的文章寥寥无几,实现过程基本靠自己摸索。
- 官方文档:https://github.com/remix-run/react-router/blob/main/docs/api.md
- 目前官方文档只有英文版,升级指南啥的网上能搜到中文的文章。
- 这里只提一下新增的一个api:
useRoutes
。
useRoutes
可以读取一个路由配置数组,生成相应的路由组件列表,类似以前的react-router-config
插件的功能,那么路由统一管理的实现用这个api就简单多了。
二、路由统一管理
实现的就是路由集中在一个文件里通过数组统一管理配置。
(这里仅介绍路由统一管理的配置方式示例,非完整代码)
1、路由配置文件
项目src/router/index.js
里填写路由配置:
import Index from '@/views/index/index'
import Login from '@/views/login/index'
import Page404 from '@/views/test/page404'
const routes = [
{
path: '/index',
element: <Index />,
},
{
path: '/login',
element: <Login />,
},
{
path: '*',
element: <Page404 />,
},
]
export {
routes
}
- 这里仅作为路由统一管理配置的参考,后面配置路由拦截时会做修改。
2、引入路由
引用路由并渲染的核心是利用react-router v6 的官方api:useRoutes
。
(1)项目入口文件src/index.js
里引入router组件:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { BrowserRouter } from 'react-router-dom'
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
)
- 路由 browser / history 模式使用
BrowserRouter
,hash 模式使用HashRouter
。 - 如果项目部署在服务器域名子目录下,就给
BrowserRouter
配置basename属性。
(2)项目入口组件src/App.jsx
里引入routes配置:
import { useRoutes } from 'react-router-dom'
import { routes } from '@/router'
function App () {
const elements = useRoutes(routes)
return elements
}
export default App
useRoutes
只能作用于router context中,所以useRoutes
需要写在一个子组件里被BrowserRouter
引用。
三、全局路由拦截
实现路由全局拦截,来自定义一些判断处理。
实现思路就是控制路由配置的element属性。
(这里仅贴出部分核心代码,至于使用方式和查看完整代码,可直接跳到下面的:“四、使用插件”)
1、路由及拦截函数配置
src/router/index.js
:
// 全局路由配置
const routes = [
{
path: '/',
redirect: '/index',
},
{
path: '/index',
component: () => import(/* webpackChunkName: "index" */ '@/views/index/index'),
meta: {
title: '首页',
needLogin: true,
},
},
{
path: '/login',
component: () => import(/* webpackChunkName: "login" */ '@/views/login/index'),
meta: {
title: '登录',
},
},
{
path: '*',
component: () => import(/* webpackChunkName: "404" */ '@/views/test/page404'),
meta: {
title: '404',
},
},
]
/**
* @description: 全局路由拦截
* @param {string} pathname 当前路由路径
* @param {object} meta 当前路由自定义meta字段
* @return {string} 需要跳转到其他页时,就返回一个该页的path路径,或返回resolve该路径的promise对象
*/
const onRouteBefore = ({ pathname, meta }) => {
// 动态修改页面title
if (meta.title !== undefined) {
document.title = meta.title
}
// 判断未登录跳转登录页
if (meta.needLogin) {
if (!isLogin) {
return '/login'
}
}
}
export {
routes,
onRouteBefore,
}
- 只对component属性配置的组件处理为懒加载,不想用懒加载的就用element属性配置组件。
- 自定义的meta数据也会以
_meta
字段名作为属性传给了每个路由组件,以备不时之需。 - routes 是原始路由配置数据,有需要时可以读取使用。比如后台管理系统中可以根据此数据自动生成导航菜单,同时可通过meta字段配置路由权限方案。
- 对于拦截函数onRouteBefore的返回值,如果存在异步的判断处理,可以return一个promise对象,promise里把要跳转的路由路径resolve出来即可。(不过这里的异步处理并不完美,实际上是先跳转原路由,再等异步完成后跳转另一路由。由于是基于react-router的element属性配置方案,暂未找到更好的解决方式。)
2、路由拦截处理
封装了页面路由容器组件,就是对路由做了一个包装,在路由渲染的时候就会执行里面的逻辑,然后调用路由拦截,做统一的路由前置钩子,既能做统一处理,也能控制拦截跳转。
guard.jsx
:
const pathRes = onRouteBefore({ pathname, meta })
const pathResType = Object.prototype.toString.call(pathRes).match(/s(w+)]/)[1]
if (pathResType === 'Promise') {
pathRes.then(res => {
if (res && res !== pathname) {
navigate(res, { replace: true })
}
})
} else {
if (pathRes && pathRes !== pathname) {
element = <Navigate to={pathRes} replace={true} />
}
}
return element
3、路由配置转换
将自定义的路由配置数组转换为react-router官方需要的路由数组,然后通过useRoutes
方法来引用。
fn.js
:
/**
* @description: 路由配置列表数据转换
* @param {string} redirect 要重定向的路由路径
* @param {function} component 函数形式import懒加载组件
* @param {object} meta 自定义字段
*/
transformRoutes (routeList = this.routes) {
const list = []
routeList.forEach(route => {
const obj = { ...route }
if (obj.path === undefined) {
return
}
if (obj.redirect) {
obj.element = <Navigate to={obj.redirect} replace={true}/>
}
if (obj.component) {
obj.element = this.lazyLoad(obj.component, obj.meta)
}
delete obj.redirect
delete obj.component
delete obj.meta
if (obj.children) {
obj.children = this.transformRoutes(obj.children)
}
list.push(obj)
})
return list
}
四、使用插件
目前上述完整代码已封装成插件,并上传到了npm。
- github地址:react-router-waiter,欢迎star ~
1、安装
npm i react-router-waiter -S
2、使用
// 在项目入口文件index.js或入口组件App.js里引入
import { HashRouter } from 'react-router-dom' // 引入react-router-dom的官方路由组件
import RouterWaiter from 'react-router-waiter' // 引入插件
import routes from './router' // 引入你的路由配置
import onRouteBefore from './onRouteBefore' // 引入你定义的路由拦截函数
function App () {
return (
<HashRouter>
<RouterWaiter
routes={routes}
onRouteBefore={onRouteBefore}
/>
</HashRouter>
)
}
export default App
3、配置路由
(示例:)
const Index = () => import(/* webpackChunkName: "index" */ '@/views/index/index')
const Login = () => import(/* webpackChunkName: "login" */ '@/views/login/index')
const Page404 = () => import(/* webpackChunkName: "404" */ '@/views/test/page404')
const routes = [
{
path: '/',
redirect: '/index', // redirect,要重定向的路由路径
},
{
path: '/index',
component: Index, // component,懒加载方式引入的组件
meta: { // meta,自定义的数据
title: '首页',
needLogin: true,
},
},
{
path: '/login',
component: Login,
meta: {
title: '登录',
},
},
{
path: '*',
component: Page404,
meta: {
title: '404',
},
},
]
export default routes
- 目前支持配置的字段有 redirect、component、meta,其他字段和react-router-dom的官方支持字段保持一致。(优先级:redirect > element > component。)
4、配置路由拦截函数
(示例:)
/**
* @description: 全局路由拦截
* @param {string} pathname 当前路由路径
* @param {object} meta 当前路由自定义meta字段
* @return {string} 需要跳转到其他页时,就返回一个该页的path路径,或返回resolve该路径的promise对象
*/
const onRouteBefore = ({ pathname, meta }) => {
// 动态修改页面title
if (meta.title !== undefined) {
document.title = meta.title
}
// 判断未登录跳转登录页
if (meta.needLogin) {
if (!isLogin) {
return '/login'
}
}
}
export default onRouteBefore
5、API
组件 RouterWaiter 的配置 API:
routes
,数组类型,路由配置数组(必填)onRouteBefore
,函数类型,路由拦截函数(可选)loading
,组件类型,懒加载路由切换时的 loading 效果组件,默认为一个空div标签(可选)
五、其他
1、后台管理系统
(个人搭建的react后台管理系统项目:github传送门,目前正逐步完善,路由配置就是使用的当前方案,供参考学习,欢迎star。)
- 嵌套路由配置方式
路由配置文件:/src/router/index.js
import PageLayout from '@/components/Layout/index'
const routes = [
{
path: '/',
redirect: '/index',
},
{
path: '/',
element: <PageLayout />,
children: [
{
path: 'index',
component: () => import(/* webpackChunkName: "index" */ '@/views/index/index'),
meta: {
title: '首页',
needLogin: true,
roleId: 10000,
},
},
...... // 这里添加其他需要PageLayout页面布局的路由
]
},
...... // 这里添加不需要PageLayout布局的路由,例如登录注册页
]
页面整体布局容器组件:/src/components/PageLayout/index.jsx
import SideBar from './sideBar' // 自定义的侧边栏
import HeadBar from './headBar' // 自定义的顶部头
import { Outlet } from 'react-router-dom' // 子路由出口,类似vue的router-view
function PageLayout () {
return (
<div className="c-Layout-index">
<SideBar />
<div className="appMainWrap">
<HeadBar />
<div className="appMain">
<Outlet />
</div>
<Outlet />
</div>
</div>
)
}
export default PageLayout
侧边栏菜单根据路由配置文件动态生成即可,需要权限判断的通过路由meta字段里自定义权限id(roleId)来匹配。
2、权限设计
使用本路由方案来进行前端react项目的权限设计也非常方便。
具体的设计方案可以参考这篇文章:传送门。