React RouterReact 中比较流行的路由组件库, 到了v4版本,react router 抛弃了配置化路由(v4 版本的路由配置官方提供了一个库,也可以直接使用,目前依然是试验阶段,可以尝试)和钩子函数;v4 的设计更加接近 React 思想,更加自由,但对于开发人员来说做的事情却更多了。本文主要结合官方的静态路由配置和 react router 的特性实现了一个相对通用的配置和钩子函数。
仅供参考和学习; 涉及的知识点

最后达到的目标

  • 自动查找路由
  • 多层级路由配置
  • 兼容官方静态路由配置
  • 兼容官方路由组件的 props
  • 每一个路由均支持 Hook 函数
  • Hook 函数支持中断、跳转

一、自动查找路由

主要借助 Webpack require.context 实现

传递给 require.context 的参数必须是字面量(literal)!
具体参数说明请参考官方说明

  • 通过 require.context 查找 modules 目录下所有的以 .route.js 结尾的文件, .route.js 结尾的文件就是静态路由配置文件,可以有多个和一个;主要考虑多人开发时的解耦,互不影响所以允许不同的开发者开发不同的模块配置多个路由;然后有该功能同意发现注册即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    let routes = []
    // 找到所有的静态路由配置
    const context = require.context('../modules', true, /\.route\.js$/)
    context.keys().forEach(routesFile => {
    const _routesModule = context(routesFile)
    // 支持es6和es5
    let routesModule = _routesModule.default || _routesModule
    // 支持导出function
    if (typeof routesModule === 'function') {
    routesModule = routesModule()
    }
    if (!isEmpty(routesModule)) {
    if (Array.isArray(routesModule)) {
    // 支持导出/函数返回数组
    routes = routes.concat(routesModule)
    } else if (isPlainObject(routesModule)) {
    // 支持导出/函数返回对象
    routes.push(routesModule)
    }
    }
    // 其他形式的导出不考虑
    })
    return routes
  • 路由配置示例 main.route.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const Login = () => <>Login Page</>
    const Register = () => <>Register Page</>
    export default [
    {
    path: '/login',
    component: Login
    },
    {
    path: '/register',
    component: Register
    }
    ]

详细配置参数参考 附录一

二、多层级路由

多层级路由主要有 routes 指定,作为子级路由配置
路由配置示例

1
2
3
4
5
6
7
8
9
10
11
12
export default [
{
path: '/system',
children:()=><div>系统管理</div>,
routes: [
{
path: '/system/user',
children: ()=> <>用户管理</>
}
]
}
]

三、钩子函数添加

静态路由和多层级路由配置有了,我们只需要再配置中添加一个钩子函数,然后使用 react routerroute 组件的 render 进行拦截,做一些前置处理;比如:中断路由、跳转路由、权限验证等等
示例配置

这里我们添加一个叫 beforeEnter 的钩子函数

1
2
3
4
5
6
7
8
9
export default [
{
path: '/',
children:()=> <div>首页</div>,
beforeEnter: (routeProps, extraProps)=>{

}
}
]

钩子函数处理

此方法参考自官方静态路由配置 renderRoutes

我们一般需要提供一个全局钩子,这里我们只需要将根路由做特殊处理,即可提供一个全局钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let rootRendered = false
renderRootRoutes(switchProps = {}) {
if (rootRendered) {
// 根级路由值渲染一次
return
}
rootRendered = true
return this.renderRoutes([
{
path: '/',
// root hooks
// default doing nothing
// 全局钩子
beforeEnter: ()=>{},
render: ({ route, ...props }) => {
const { routes } = route
return this.renderRoutes(routes, props, switchProps)
},
// 渲染子级路由
// 除了此路由,其他路由均认为是子级路由
routes: this.routes
}
])
}

处理下级路由的渲染

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
renderRoutes(routes, extraProps = {}, switchProps = {}) {
if (!this.rootRendered) {
throw new Error(`you must render root routes first.`)
}
return routes ? (
<Switch {...switchProps}>
{routes.map((route, i) => {
// render、 children、 component为路由参数,我们直接支持
// beforeEnter 是我们添加的钩子函数
const { order, key, render, children: CComponent, component: Component, beforeEnter, ...routeProps } = route
return (
<Route
key={key || i}
{...routeProps}
render={props => {
// 如果有beforeEnter并且是函数,我们就执行该函数
if (beforeEnter && typeof beforeEnter === 'function') {
const next = beforeEnter(props, routeProps)
if (next !== null && next !== undefined && next !== true) {
// 默认情况如果钩子函数返回null、undefined、true 我们即认为通过,不做任何处理
if (next === false) {
// 返回false 表示中断
return null
}
if (typeof next === 'string') {
// 如果返回字符串,使用 Redirect 进行跳转
return <Redirect to={next} from={props.location.pathname} />
}
if (typeof next === 'function' || React.isValidElement(next)) {
// 如果返回的function 或 element,默认要求返回的是组件
// 这里使用 React Provider 将子路由传递下去,并使用 Consumer 方便做下级路由的渲染
const NextComponent = next
return (
<Provider value={{ route: routeProps }}>
<NextComponent {...props} {...extraProps} route={routeProps} />
</Provider>
)
}
if (isPlainObject(next)) {
// 如果返回的是对象,要求返回的是 Route和Redirect支持的属性
// 并使用 redirect 参数标记是否需要Redirect
const { redirect, ...nextProps } = next
if (redirect === true) {
return <Redirect {...nextProps} {...{ from: props.location.pathname }} />
}
return <Route {...nextProps} />
}
warn(`"${props.location.pathname} => beforeEnter"
hook return values error. expected null? undefined? true? React.Component? HTMLElement? Route props?
route props detail to see
https://reacttraining.com/react-router/web/api/Route
https://reacttraining.com/react-router/web/api/Redirect`
)
return null
}
}
// 直接渲染
// Component.prototype.isReactComponent
// 1. children -> 2. component -> 3. render
return CComponent ?
(
<Provider value={{ route: routeProps }}>
<CComponent {...props} {...extraProps} route={routeProps} />
</Provider>
)
:
(
Component ?
(
<Provider value={{ route: routeProps }}>
<Component {...props} {...extraProps} route={routeProps} />
</Provider>
)
:
(
render ? render({ ...props, ...extraProps, route: routeProps }) : null
)
)

}}
/>
)
})}
</Switch>
) : null
}

下级路由渲染语法糖

下级路由渲染可以直接使用 renderRoutes 方法进行渲染; 但这样处理不怎么优雅,且需要明确指定下级路由,若不使用 ProviderConsumer 组合下级路由参数还需要层层传递,过于麻烦。这里我们参考 vuerouter-view 提供一个 renderRoutes 语法糖的组件;方便渲染下级路由。

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
class RouteView extends React.PureComponent {
render() {

// find parent props
const { route = {}, ...props } = this.props
// 跟级路由渲染
if (!rootRendered) {
return renderRootRoutes(props)
} else if (Array.isArray(route.routes) && route.routes.length > 0) {
// 下级路由渲染
// 优先使用 props 参数中的子级路由信息
return renderRoutes(route.routes, props)
}
// 此信息由 renderRoutes 方法中 Provider 提供
return (
<Consumer>
{
({ route }) => {
const { routes, ...contextProps } = route
if (Array.isArray(routes) && routes.length > 0) {
return renderRoutes(routes, contextProps)
}
return null
}
}
</Consumer>
)
}
}
export const RenderRouteView = RouteView

经过以上的处理我们基本完成了既定的目标

附录一

路由配置参数

1
2
3
4
5
6
7
8
9
10
11
{
path: '',
routes: '',
key: null,
order: 99,
beforeEnter: null
children: null,
component: null,
render: null,
exact: false,
}

path
路由路径
默认值: 无

routes
子级路由配置,属性相同

key
路由唯一标记
默认值: 序号

order
用于给路由排序,React Router v4 中使用 Switch 并提供一个无 pathRoute 放在最后可以当做 404 处理;静态路由配置若有多个配置文件,可能会出现顺序问题,这里采用order 来明确指定排序规则;只在某个层级中有效。
默认值: 99

beforeEnter
钩子函数
默认值:无

children
Route.children

component
Route.component

render
Route.render

exact
Route.exact

其他参数同 Route

附录二

源码目前尚无法提供,关注公众号,可以第一时间接收到通知