本文将用尽可能容易理解的方式,实现最小可用的 react-router v4 和 history,目的为了了解 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,我们接着往下看。
3. Link 的实现
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
| 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 项目点个⭐️吧!