实现一个 react-router

本文将用尽可能容易理解的方式,实现最小可用的 react-router v4history,目的为了了解 react-router 实现原理。

一、开始之前

在开始阅读本文之前,希望你至少使用过一次 react-router,知道 react-router 的基本使用方法。

二、已实现的功能

  • 根据当前页面的 location.pathname,渲染对应 Route 中的 component
  • 点击 Link,页面无刷新,pathname 更新,渲染对应 Route 中的 component
  • 浏览器后退/前进,页面无刷新,渲染对应 Route 中的 component

三、Github 地址与在线预览

四、原理分析

1. Route 的实现

先来看一段代码,我们需要实现的逻辑是:当 location.pathname = '/' 时,页面渲染 Index 组件,当 location.path = '/about/' 时,页面渲染 About 组件。

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
import React from 'react';

import { BrowserRouter as Router, Route, Link } from "./react-router-dom";

function Index(props) {
console.log('Index props', props);
return <h2>Home</h2>;
}

function About() {
return <h2>About</h2>;
}

function Users() {
return <h2>Users</h2>;
}

function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about/">About</Link>
</li>
<li>
<Link to="/users/">Users</Link>
</li>
</ul>
</nav>

<Route path="/" exact component={Index} />
<Route path="/about/" component={About} />
<Route path="/users/" component={Users} />
</div>
</Router>
);
}

export default App;

其实,Route 组件内部的核心逻辑就是判断当前 pathname 是否与自身 props 上的 path 相等,如果相等,则渲染自身 props 上的 component,不等的时候不渲染,返回 null。

好,来看下 Route 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import { RouterContext } from './BrowserRouter';

export default class Route extends React.Component {
render() {
const { path, component } = this.props;
if (this.context.location.pathname !== path) return null;
return React.createElement(component, { ...this.context })
}
}

Route.contextType = RouterContext

Route 主要就是一个 render() 函数,内部通过 context 获得当前 pathname。那么这个 context 是哪来的呢?

2. BrowserRouter 的实现

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
import React from 'react';
import { createBrowserHistory } from '../history';

const history = createBrowserHistory()

export const RouterContext = React.createContext(history)

export default class BrowserRouter extends React.Component {
constructor(props) {
super(props)
this.state = {
location: {
pathname: window.location.pathname
}
}
}

render() {
const { location } = this.state;
return (
<RouterContext.Provider value={{ history, location }}>
{this.props.children}
</RouterContext.Provider>
)
}
};

这里仅贴出了首次渲染的逻辑代码。BrowserRouter 在 constructor 中根据 window.location 初始化 location,然后将 location 传入 RouterContext.Provider 组件,子组件 Route 接收到含有 location 的 context,根据 1. Route 的实现 完成首次渲染。

注意到传入 RouterContext.Provider 组件的对象不光有 location,还有 history 对象。这个 history 是做什么用的呢?其实是暴露 history.push 和 history.listen 方法,提供给外部做跳转和监听跳转事件使用的。Link 组件的实现也是用到了 history,我们接着往下看。

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

export default class Link extends React.Component {
constructor(props) {
super(props)
this.clickHandler = this.clickHandler.bind(this)
}

clickHandler(e) {
console.log('click', this.props.to);
e.preventDefault()
this.context.history.push(this.props.to)
}

render() {
const { to, children } = this.props;
return <a href={to} onClick={this.clickHandler}>{children}</a>
}
}

Link.contextType = RouterContext

Link 组件其实就是一个 a 标签,与普通 a 标签不同,点击 Link 组件并不会刷新整个页面。组件内部把 a 标签的默认行为 preventDefault 了,Link 组件从 context 上拿到 history,将需要跳转的动作告诉 history,即 history.push(to)

如下面代码所示,BrowserRouter 在 componentDidMount 中,通过 history.listen 监听 location 的变化。当 location 变化的时候,setState 一个新的 location 对象,触发 render,进而触发子组件 Route 的重新渲染,渲染出对应 Route。

1
2
3
4
5
6
7
// BrowserRouter
componentDidMount() {
history.listen((pathname) => {
console.log('history change', pathname);
this.setState({ location: { pathname } })
})
}

4. history 的实现

history 的内部实现是怎么样的呢?请看下面的代码:

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
let globalHistory = window.history;

export default function createBrowserHistory() {
let listeners = []

const push = function (pathname) {
globalHistory.pushState({}, '', pathname)
notifyListeners(pathname)
}

const listen = function (listener) {
listeners.push(listener)
}

const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args))
}

window.onpopstate = function () {
notifyListeners(window.location.pathname)
}

return {
listeners,
listen,
push
}
};

history 通过 listen 方法收集外部的监听事件。当外部调用 history.push 方法时,使用 window.history.pushState 修改当前 location,执行 notifyListeners 方法,依次回调所有的监听事件。注:这里为了让代码更加容易理解,简化了 listener this 上下文的处理。

另外,history 内部增加了 window.onpopstate 用来监听浏览器的前进后退事件,执行 notifyListeners 方法。

五、总结

我们使用了 100 多行代码,实现了 react-router 的基本功能,对 react-router 有了更深入的认识。想更加深入的了解 react-router,建议看一下 react-router 的源码,快速走读一遍,再对比下本文的实现细节,相信你会有一个更清晰的理解。

觉得本文帮助到你的话,请给我的 build-your-own-react-router 项目点个⭐️吧!

评论