React Router迷之实现

本文主要是通过创建一个基本版本的React Router v4 来理解背后实现的原理。如果要学习React Router怎么使用,请移步官方文档

Route

Route是用来渲染 UI的,具体点就是当一个 URL 匹配上了你所指定的路由路径,就进行渲染

Route 组件常用的属性主要有四个exactpathcomponentrender,这个四个属性的主要作用为:
exact: 只有当所给路径精确匹配上 location.pathname 时才返回 true。
path: 非必须属性,如果改属性不存在,那么路由对应的组件将自动渲染
component: 如果路径匹配上了,则渲染属性对应的组件
render: 允许你创建一个直接返回 UI 的内联函数而不用创建额外的组件

Route的功能是渲染匹配指定路由路径的组件,所以Router需要做到的功能为:判断当前的URL是否和组件的path相匹配,如果匹配则返回渲染的UI;否则返回null

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
// 判断path是否匹配
const matchPatch = (pathname, options) => {
const { exact = false, path } = options
if (!path) {
return {
path: null,
url: pathname,
isExact: true,
}
}
const match = new RegExp(`^${path}`).exec(pathname)
if(!match){
// 没有匹配上
return null;
}
const url = match[0];
const isExact = url === patchname;
if(exact && !isExact){
// 匹配上了 , 但是不精确
return null;
}
return {
path,url,isExact
}
}
class Route extend React.Component{
static propTypes = {
exact: PropTypes.bool,
path: PropTypes.string,
component: PropTypes.func,
r ender: PropTypes.func,
}
render(){
let {path,render,component,exact} = this.props;
let match = matchPath(location.pathname,{path,exact});

if(!match){
// path匹配失败, 返回null
return null;
}
// path 匹配成功
if(component){
// 如果component属性存在
// 以 component 创建新元素并且传递 match
return React.createElement(component,{match,loaction})
}
if(render){
// 如果render存在
// 则调用 render 并以 match 作为参数
return render({match,loction})
}
return null;

}
}

上面的代码即实现了:如果匹配上了 path 属性,就返回 UI,否则什么也不做

这里还有一种情况就是点击浏览器前进/后退按钮改变URL的时候,需要让Route可以做出针对性的处理。
当用户点击了后退/前进按钮的时候,popstate事件会被触发,因此只需要监听popstate事件,在URL被改变时,触发popstate去检查是否匹配上了新的 URL,如果是则渲染 UI,如果不是,什么也不做。

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
// 判断path是否匹配
const matchPatch = (pathname, options) => {
const { exact = false, path } = options
if (!path) {
return {
path: null,
url: pathname,
isExact: true,
}
}
const match = new RegExp(`^${path}`).exec(pathname)
if(!match){
// 没有匹配上
return null;
}
const url = match[0];
const isExact = url === patchname;
if(exact && !isExact){
// 匹配上了 , 但是不精确
return null;
}
return {
path,url,isExact
}
}
class Route extend React.Component{
static propTypes = {
exact: PropTypes.bool,
path: PropTypes.string,
component: PropTypes.func,
render: PropTypes.func,
}

componentDidMount(){
// 加了一个 popstate 监听,当 popstate 触发的时候,调用 forceUpdate 来强制做重新渲染的判断。
window.addEventListener("popstate",this.handlePopstate);
}
componentWillUnMount(){
// 组件卸载时,移除监听事件
window.removeEventListener("popstate",this.handlePopstate);
}
handlePopstate(){
this.forceUpdate();
}
render(){
let {path,render,component,exact} = this.props;
let match = matchPath(location.pathname,{path,exact});

if(!match){
// path匹配失败, 返回null
return null;
}
// path 匹配成功
if(component){
// 如果component属性存在
// 以 component 创建新元素并且传递 match
return React.createElement(component,{match,loaction})
}
if(render){
// 如果render存在
// 则调用 render 并以 match 作为参数
return render({match,loction})
}
return null;

}
}

这样就实现了根据后退/前进按钮来“重匹配”、“重判断”和“重渲染”。

Link主要是解决通过a标签改变URL的时候,重新匹配Route组件并渲染的问题。
Link一般是这样的使用的<Link to='/some-path' replace={false} />to是一个string类型,表示要跳转到的链接。replace是布尔类型,如果为true,则将替换history中的最后一个链接替换为当前的链接,否则就添加当前链接到history中。

首先,Link需要渲染一个a标签,并且需要组织a的默认动作,以免全页面刷新,所以需要一个click事件处理函数来阻止a的默认动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Link extends React.Component{
static propTypes = {
to: PropTypes.string.isRequired,
replace: PropTypes.bool
}
onClick(e){
e.preventDefault();
let {to,replace} = this.props;
}
render(){
return (
<a href={this.props.to} onClick={this.handleClick}>{this.props.children}</a>
)
}
}

然后需要处理更新URL的部分,通过historyapi更新路由,我们需要在点击事件中,获取目标URL,然后通知目标URL对应的Route组件渲染。

为了可以准确的渲染路由对应的组件,我们需要将所有的路由收集起来,没当地址发生改变的时候,就遍历数组,并调用forceUpdate函数。

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
// 这里创建的两个函数,用来收集Route和删除Route
let instances = [];
const register = (comp)=>{instances.push(comp);};
const unregister = (comp)=>{instances.splice(instances.indexOf(comp),1);}

// 更新 Route 组件
// 判断path是否匹配
const matchPatch = (pathname, options) => {
...
}
class Route extend React.Component{
...

componentDidMount(){
// 加了一个 popstate 监听,当 popstate 触发的时候,调用 forceUpdate 来强制做重新渲染的判断。
window.addEventListener("popstate",this.handlePopstate);
register(this);
}
componentWillUnMount(){
// 组件卸载时,移除监听事件
window.removeEventListener("popstate",this.handlePopstate);
unregister(this)
}
handlePopstate(){
this.forceUpdate();
}
...
}

// 更新 Link 组件
class Link extends React.Component{
static propTypes = {
to: PropTypes.string.isRequired,
replace: PropTypes.bool
}
onClick(e){
e.preventDefault();
let {to,replace} = this.props;
if(replace){
history.replaceState({},null,to)
}else{
history.pushState({},null,to)
}
instances.forEach(item=>item.forceUpdate());
}
render(){
return (
<a href={this.props.to} onClick={this.handleClick}>{this.props.children}</a>
)
}
}

Redirect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Redirect extends React.Component{
static defaultProps={
push:false
}
static propTypes = {
push: PropTypes.bool.isRequired,
to: PropTypes.string.isRequired
}
componentDidMount(){
let {to,push} = this.props;
if(push){
history.pushState({},null,to);
}else{
history.replaceState({},null,to);
}
}
render(){
return null;
}
}

参考

https://www.jianshu.com/p/4e86372cb2fb