学习目标

通过对 react-router 的学习和自己实现其核心功能,更加深度的掌握 react-router 的使用和原理,以达到日常遇到相关问题可以从源码角度快速定位问题并解决问题;(记笔记行为,仅供学习参考,代码解释均在注释里)

react-router 简介

react-router 包含 3 个库,react-router、react-router-dom 和 react-router-native。react-router 提供 最基本的路由功能,实际使用的时候我们不会直接安装 react-router,而是根据应用运行的环境选择安 装 react-router-dom(在浏览器中使用)或 react-router-native(在 rn 中使用)。react-router-dom 和 react-router-native 都依赖 react-router,所以在安装时,react-router 也会自动安装。

使用 demo

  1. 创建HomePageLoginPageUserPage_404Page四个页面;
  2. 然后在 App 页面引入并使用,写一个简单的使用例子;

页面效果如下:
图一 四个跳转,点击跳转相应页面
图二 测试Prompt,点击‘go home’按钮跳出提示

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import React, { Component } from 'react'
import {
BrowserRouter as Router,
Route,
Link,
Switch,
useHistory,
useLocation,
useRouteMatch,
useParams,
withRouter,
Redirect,
Prompt,
} from './my-router-dom'

import HomePage from './pages/HomePage'
import UserPage from './pages/UserPage'
import LoginPage from './pages/LoginPage'
import _404Page from './pages/_404Page'

function App() {
return (
<div className="App">
<Router>
<Link to="/">首页</Link>
<Link to="/user">用户中心</Link>
<Link to="/login">登录</Link>
<Link to="/product/123">商品</Link>

{/*Switch 独占路由: 返回第一个匹配的route或者redirect */}
<Switch>
{/* children > component > render */}
<Route
path="/"
exact
// children={children}
component={HomePage}
// render={render}
></Route>
<Route path="/user" exact component={UserPage} />
<Route path="/login" exact component={LoginPage} />
{/* 动态路由 测试 */}
<Route
path="/product/:id"
component={Product}
// render={(props) => <Product {...props} />}
/>
<Route component={_404Page} />
</Switch>
</Router>
</div>
)
}

export default App

/* 测试函数组件使用 */
// function Product() {
// // const histroy= useHistory();
// // const location= useLocation();
// const match = useRouteMatch()
// const { url } = match
// const { id } = useParams()

// return (
// <div>
// <h1>Search-{id}</h1>
// <Link to={url + '/detail'}>详情</Link>
// <Route path={url + '/detail'} component={Detail} />
// </div>
// )
// }

/* 测试类组件使用 */
@withRouter
class Product extends Component {
constructor(props) {
super(props)
this.state = { confirm: true }
}
render() {
console.log('Product', this.props) //sy-log
return (
<div>
<h3>Product</h3>
<Link to="/"> go home </Link>
{/*测试 Prompt message可以传入字符串,也可以传入函数 */}
<Prompt
when={this.state.confirm}
message="那我走?"
// message={(location) => {
// return "你确定要离开页面吗";
// }}
/>
</div>
)
}
}

function Detail({ match }) {
return (
<div>
<h1>detail</h1>
</div>
)
}

// 测试route渲染顺序使用
// children > component > render
function children(props) {
console.log('children props', props) //sy-log

return <div>children</div>
}

function render(props) {
console.log('render props', props) //sy-log
return <div>render</div>
}

实现

BrowserRouter

使用 HTML5 提供的 history API ( pushState , replaceStatepopstate 事件) 来保持 UI 和 URL 的同步。
react-router使用的是 history 这个库来进行路由的处理,提供了操作路由的API并帮我们抹平了平台差异,可以很放心的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react'
import { createBrowserHistory } from 'history' // 该库为浏览器抹平平台差异
import Router from './Router'

class BrowserRouter extends React.Component {
constructor(props) {
super(props)
// 创建 history
// 使用什么路由模式在这里控制 然后通过context传个子级
this.history = createBrowserHistory()
}

render() {
return <Router history={this.history} children={this.props.children} />
}
}

export default BrowserRouter

HashRouter

使用 URL 的 hash 部分(即 window.location.hash )来保持 UI 和 URL 的同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react'
import { createHashHistory } from 'history'
import Router from './Router'

class BrowserRouter extends React.Component {
constructor(props) {
super(props)
this.history = createHashHistory()
}

render() {
return <Router history={this.history} children={this.props.children} />
}
}

export default BrowserRouter

Router

Router 组件的作用是将父级的路由模式传给 Switch、Router、Redirect 等子组件,并监听路由变化且刷新页面;

  1. 通过Context进行子父组件数据的传递;
  2. 通过history.listen监听路由,并在组件卸载时取消监听;
  3. location存在state中,利用setState改变值会刷新页面达到重新渲染页面的效果;
  4. match是对location.pathnameRoute组件配置的path属性做匹配的结果,Router组件中将match设置默认匹配根目录,页面默认首先渲染根目录匹配的组件;
  5. 其中RouterContext位置在代码在目录 > 其他组件 > RouterContext;
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
import React from 'react'
import RouterContext from './RouterContext.js'

// 用什么路由模式 由父级决定 BrowserRouter HashRouter MemoryRouter
// Router 需要做的是拿到父级传来的history 并对路由变化做监听
class Router extends React.Component {
// 赋一个初始值
static computeRootMatch(pathname) {
return {
path: '/',
url: '/',
params: {},
isExact: pathname === '/',
}
}
constructor(props) {
super(props)

this.state = {
location: props.history.location, // 记录一下当前的location 为监听location的变化做准备
}

// 监听路由的变化,
this.unlisten = props.history.listen((location) => {
this.setState({ location })
})
}

componentWillUnmount() {
// 组件卸载 关掉监听
this.unlisten()
}

render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
}}
>
{this.props.children}
</RouterContext.Provider>
)
}
}

export default Router

Route

  1. Route 组件主要作用是对location.pathnameprops.path做匹配,如果匹配到就返回match的值,如果没匹配到 match 为 null;
  2. 如果path不存在,则 Router 传下来的context.match作为默认值,这样保证 match 存在才能渲染这个没有path的组件,例如 404 页面;
  3. Route 组件最重要的是对 children、component、render 三个属性的渲染规则,详细规则写在了代码注释里,请直接看下面的代码;
  4. 其中的matchpath是对location和path做匹配的函数,代码在目录 > 其他组件 > matchpath;
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
import React from 'react'
import RouterContext from './RouterContext'
import matchPath from './matchPath'

class Route extends React.Component {
constructor(props) {
super(props)
}

render() {
return (
<RouterContext.Consumer>
{(context) => {
const { location } = context
const { path, children, component, render } = this.props
// 1.路由的匹配
// Public API for matching a URL pathname to a path.
// 404页面没有path 就使用上级传来的默认match
const match = path
? matchPath(location.pathname, this.props)
: context.match

// 传给组件的props router-dom 把history、location等路由的数据传给组件
const props = {
...context,
location,
match,
}

console.log('match', match)
// 组件的严格匹配
// return match? React.createElement(component) :null
// match children > component > render > null
// no match : children(function) > null
// 详细解析: 1. 先判断match是否存在(存在则找到了路由对应的组件),
// 2. 存在的话查找children属性是否存在,如果存在:判断children的类型是否是函数形式,如果是函数形式需要把props传给children,不是函数形式直接渲染children
// 3. children不存在:继续找component属性是否存在,component存在就直接通过React.createElement创建组件;
// 4. component不存在:查找render属性存不存在,render属性存在就执行render函数并将props传给render,render不存在就直接返回为空;
// 5. match如果不存在,渲染函数子组件

// 由于context的取值规则是 取到最近一层的provide传进来的值
// 这里再包一层 <RouterContext.Provider是为了写hook useRouteMatch的时候,route可以拿到当前匹配的match传给组件,不传这一层,组件拿到的match就会是最顶级router(browserRouter/hashRouter)传来的默认match
return (
<RouterContext.Provider value={props}>
{match
? children
? typeof children === 'function'
? children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === 'function'
? children(props)
: null}
</RouterContext.Provider>
)
}}
</RouterContext.Consumer>
)
}
}

export default Route

Link.js: 跳转链接,处理点击事件

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
import React, { useContext } from 'react'
import RouterContext from './RouterContext'

/**
* Link 本质就是a标签
*/
function Link({ to, children , ...rest}) {
// 拿到父级传来的 history
const context = useContext(RouterContext)

const handleClick = (e) => {
e.preventDefault()
// 更换路由
context.history.push(to)
}

return (
<a href={to} onClick={handleClick} {...rest}>
{children}
</a>
)
}

export default Link

Switch

  1. Switch组件的主要作用是独占路由,保证当前页面只渲染 match 匹配的一个组件;
  2. 不加Switch组件,Route组件的 children 属性渲染层级比较高,则都会被渲染;
  3. Switch中对子组件进行循环匹配,如果找到match则通过React.cloneElement渲染对应组件,没有匹配就渲染404页面(假设Route配置了404页面);
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
import React from 'react'
import RouterContext from './RouterContext'
import matchpath from './matchPath'

/**
* switch独占路由,返回第一个匹配的route或者redirect
*/
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{(context) => {
const location = context.location
// 记录是否匹配
let match
// 记录匹配的元素(route/redirect)
let element

// 利用 React.Children 对switch的子组件route们遍历
React.Children.forEach(this.props.children, (child) => {
// match为 null 证明还没匹配上,且child是有效的元素
if (match == null && React.isValidElement(child)) {
// 缓存组件
element = child
// 判断route是否存在path 如果存在就拿到path去和当前location的path做对比,不存在就使用祖传的默认match
match = child.props.path
? matchpath(location.pathname, child.props)
: context.match
}
})

return match
? React.cloneElement(element, { computedMatch: match })
: null
}}
</RouterContext.Consumer>
)
}
}

export default Switch

Redirect

Redirect组件作用是对路由进行重定向

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
import React from 'react'
import RouterContext from './RouterContext'
import LifeCycle from './LifeCycle'

class Redirect extends React.Component {
render() {
return (
<RouterContext.Consumer>
{(context) => {
const { history } = context
const { to, push = false } = this.props
return (
<LifeCycle
onMount={() => {
push ? history.push(to) : history.replace(to)
}}
/>
)
}}
</RouterContext.Consumer>
)
}
}

export default Redirect

hooks

hooks组件主要作用是给函数组件提供路由hook,确保在函数组中可以使用路由的history、location等一众属性和方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useContext } from 'react'
import RouterContext from './RouterContext'

export function useHistory() {
// 拿到Route组件Provider的props
return useContext(RouterContext).history
}

export function useLocation() {
return useContext(RouterContext).location
}

export function useRouteMatch() {
return useContext(RouterContext).match
}

export function useParams() {
const match = useContext(RouterContext).match
return match ? match.params : null
}

Prompt

Prompt组件主要作用是对路由离开之前进行提示;
例如:用户在信息填写页填写了一大堆东西且未保存,然后一不小心点击到返回按钮,这个时候需要提示用户是否离开,否则直接离开数据也没了,用户会直接崩溃~😂

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
import React from 'react'
import RouterContext from './RouterContext'
import LifeCycle from './LifeCycle'

class Prompt extends React.Component {
render() {
return (
<RouterContext.Consumer>
{(context) => {
const { message, when = true } = this.props
if (!when) return null

// 利用history库的 block 在离开页面的时候做拦截
const method = context.history.block
// const
return (
<LifeCycle
onMount={(self) => {
// 组件挂载的时候把 method 存在 release上
self.release = method(message)
}}
onUpdate={(self, prevProps) => {
if (prevProps.message !== message) {
self.release()
self.release = method(message)
}
}}
onUnmount={(self) => {
self.release()
}}
message={message}
/>
)
}}
</RouterContext.Consumer>
)
}
}

export default Prompt

WithRouter

withRouter本身是一个hoc高级组件,为类组件提供history的方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import RouterContext from './RouterContext'

/**
* withrouter本身就是一个hoc高阶组件,高阶组件接收一个组件,返回一个组件
*/
const withRouter = (WrapperComponent) => (props) => {
// 类组件通过withRouter把 router 的api传给组件
return (
<RouterContext.Consumer>
{(context) => <WrapperComponent {...props} {...context} />}
</RouterContext.Consumer>
)
}

export default withRouter

MemoryRouter

MemoryRouter直接拷贝的 react-router 源码, MemoryRouter一般写native会用到,web的话常用BrowserRouterHashRouter;

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
import React from 'react'
import PropTypes from 'prop-types'
import { createMemoryHistory as createHistory } from 'history'
import warning from 'tiny-warning'

import Router from './Router.js'

/**
* The public API for a <Router> that stores location in memory.
*/
class MemoryRouter extends React.Component {
history = createHistory(this.props)

render() {
return <Router history={this.history} children={this.props.children} />
}
}

if (__DEV__) {
MemoryRouter.propTypes = {
initialEntries: PropTypes.array,
initialIndex: PropTypes.number,
getUserConfirmation: PropTypes.func,
keyLength: PropTypes.number,
children: PropTypes.node,
}

MemoryRouter.prototype.componentDidMount = function () {
warning(
!this.props.history,
'<MemoryRouter> ignores the history prop. To use a custom history, ' +
'use `import { Router }` instead of `import { MemoryRouter as Router }`.'
)
}
}

export default MemoryRouter

其他组件

RouterContext

1
2
3
4
5
6
import React from 'react'

// 为router传递数据
const RouterContext = React.createContext()

export default RouterContext

LifeCycle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react'
/**
* LifeCycle就做一件事,在组件生命周期里do something
*/
class LifeCycle extends React.Component {
componentWillMount() {
if (this.props.onMount) this.props.onMount.call(this, this)
}

componentWillUpdate(prevProps) {
if (this.props.onUpdate) this.props.onUpdate.call(this, this, prevProps)
}

componentWillUnmount() {
if (this.props.onUnmount) this.props.onUnmount.call(this, this)
}

render() {
return null
}
}

export default LifeCycle

matchpath

path-to-regexp这个库真的蛮好用的,githubExpress Route Tester

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
import pathToRegexp from 'path-to-regexp'

const cache = {}
const cacheLimit = 10000
let cacheCount = 0

function compilePath(path, options) {
const cacheKey = `${options.end}${options.strict}${options.sensitive}`
const pathCache = cache[cacheKey] || (cache[cacheKey] = {})

if (pathCache[path]) return pathCache[path]

const keys = []
const regexp = pathToRegexp(path, keys, options)
const result = { regexp, keys }

if (cacheCount < cacheLimit) {
pathCache[path] = result
cacheCount++
}

return result
}

/**
* Public API for matching a URL pathname to a path.
*/
function matchPath(pathname, options = {}) {
if (typeof options === 'string' || Array.isArray(options)) {
options = { path: options }
}

// exact精确匹配 strict严格匹配 sensitive区分大小写
const { path, exact = false, strict = false, sensitive = false } = options

const paths = [].concat(path)

return paths.reduce((matched, path) => {
if (!path && path !== '') return null
if (matched) return matched

const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive,
})
const match = regexp.exec(pathname)

if (!match) return null

const [url, ...values] = match
const isExact = pathname === url

if (exact && !isExact) return null

return {
path, // the path used to match
url: path === '/' && url === '' ? '/' : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index]
return memo
}, {}),
}
}, null)
}

export default matchPath