在我们使用react这个库搭建前端工程的时候,是我们不可避免需要使用到第三方的路由库来划分各个模块,或者说页面,而主要的路由库就是react-router。为了进一步了解这个库是如何实现前端路由的,需要对其源码有初步的了解。
通常对于路由库功能的理解,就是监听前端路由的改变,动态渲染匹配当前路由的组件。
搭建一个mini的react-router,有助于理解这个库是如何实现前端路由功能的,对其实现的思路有基本了解。以及了解提供的hook之一useHistory的功能和实现方法。至于其他react-router的功能,在此基础上也能很好的理解。
简单案例
import React from 'react';
import {
BrowserRouter as Router,
Route,
} from './mini-react-router/react-router-dom';
import { useHistory } from './mini-react-router/react-router/hooks';
import './App.css';
function App() {
//简单案例,Route内部比较使用的===比较,因此不会渲染所有符合react-router的路由规则
return (
<Router>
<Route path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/user">
<User />
</Route>
</Router>
);
}
function Home(props) {
const history = useHistory();
return (
<div>
<h1>首页</h1>
<button onClick={() => history.push('/about')}>about</button>
<button onClick={() => history.push('/user')}>user</button>
</div>
);
}
function About() {
const history = useHistory();
return (
<div>
<h1>关于</h1>
<button onClick={() => history.push('/')}>home</button>
</div>
);
}
function User() {
const history = useHistory();
return (
<div>
<h1>我的</h1>
<button onClick={() => history.push('/')}>home</button>
</div>
);
}
export default App;
分析react-router源码会发现,在浏览器环境中,我们需要导入react-router-dom中的相关Router组件(这取决于使用的路由匹配模式histroy或者hash),那么我们需要了解下react-router库的组成。
react-router的组成
这是github中的react-router库的项目结构:
我们可以看到,react-router仓库分成4个项目:react-router-dom-v5-compat, react-router-dom, react-router-native, react-router。
-
react-router: 核心库,定义了路由的基本组件和逻辑,比如定义了Router、Route组件和相关的hook -
react-router-dom: 是在浏览器上环境中使用的库,其依赖react-router -
react-router-native: 实在react-native环境中使用的库,其依赖react-router -
react-router-dom-v5-compat此软件包通过与v5并行运行,使React Router web App能够增量迁移到v6中的最新API。它是v6的一个副本,带有一对额外的组件以保持两者的同步。
这里默认的宿主环境是浏览器,所以我们主要关注的react-router-dom和react-router组件。
另外,react-router-dom依赖独立的history库,该库的主要作用是:
允许您在JavaScript运行的任何地方轻松管理会话历史。历史对象抽象出各种环境中的差异,并提供一个最小的API,允许您管理历史堆栈、导航和会话之间的持久状态
到这里,我们mini-react-router所需要实现的依赖就全部齐活,一共是history, react-router, react-router-dom
react-router-dom
在react-router-dom中,定义了很多用于浏览器环境的组件,例如BrowserRouter、HashRouter、Switch或者Link。这其中最主要的是BrowserRouter和HashRouter这两个路由器组件。
// BrowserRouter.js
import React, { Component } from 'react';
import { createBrowserHistory as createHistory } from '../history';
import { Router } from './index';
class BrowserRouter extends Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default BrowserRouter;
// index.js
export { Route, Router } from '../react-router';
export { default as BrowserRouter } from './BrowserRouter.js';
export { default as HashRouter } from './HashRouter.js';
通过源码可以看到,BrowserRouter组件中除了创建了Browser History,使用的HTML5定义的history API,创建history的方法来自于独立的history库,只是包装了Router组件,而该组件来自react-router核心库。
那么让我们来看下另外两个依赖。
histroy
history是独立的脚本库,用于在JavaScript运行的任何地方轻松管理会话历史。历史对象抽象出各种环境中的差异,并提供一个最小的API,允许管理历史堆栈、导航和会话之间的持久状态。
function createListenerManager() {
let listeners = [];
return {
//订阅
subscribe: function (listener) {
if (typeof listener !== 'function') {
throw new Error('typeof listener must is function');
}
listeners.push(listener);
//取消订阅函数
return function unSubscribe() {
listeners = listeners.filter(function (item) {
return item !== listener;
});
};
},
//发布:调用所有监听函数,并传入新的location
notify: function (location) {
listeners.forEach((listen) => listen(location));
},
};
}
/**
* browser Histroy,用于支持histroy API的路由
*/
export function createBrowserHistory() {
const location = {
pathname: '/',
};
const lietenerManager = createListenerManager();
window.addEventListener('popstate', handlePop);
//监听事件
function listen(listener) {
return lietenerManager.subscribe(listener);
}
/**
* onpopstate事件响应函数
* 处理history变更,主要是监听浏览器的前进和回退,histroy.go、histroy.go、histroy.go也会触发onpopstate
* history.pushState和history.replaceState API调用并不会触发该事件
* @param {*} e
*/
function handlePop(e) {
lietenerManager.notify({ pathname: window.location.pathname });
}
function push(path) {
//虽然pushState不会触发popstate事件,但是不可省略
//这样可以保持state状态在当前路由记录中得到正确更改,保证state信息一致
window.history.pushState(null, '', path);
lietenerManager.notify({ pathname: path });
}
return {
listen,
location,
push,
};
}
可以看到,在BrowserRouter组件中使用的createBrowserHistory方法返回的对象中有三个基本属性:
- listen : 新增一个监听函数,用于外部订阅路由的变化
- location : 路由信息对象
- push : 给路由栈中新增一条记录
其中值得注意的是:
-
HTML5 history API虽然规定了路由变化事件onpopstate,但是受其触发的条件限制,在push过程中需要调用history.pushState来保证浏览器的路由栈一致。 -
window.addEventListener('popstate', handlePop),监听浏览器popstate事件,保证路由变化是正确响应。 -
createListenerManager实现了订阅-发布模式,统一管理路由的变化
createHashHistory返回的history接口和createBrowserHistory一致,其内部是对hash的处理。
react-router
react-router核心库,负责处理核心逻辑,适用于浏览器环境和react-native,其中定义的Router,Route组件是其中关键组件。
Router
import React from 'react';
import HistoryContext from './HistoryContext.js';
import RouterContext from './RouterContext.js';
/**
* Router组件的作用主要:
* 1. 监听history的最新location,并当作props传递给子组件
* 2. 渲染children
*/
class Router extends React.Component {
// 静态方法,计算当前pathname是否匹配根路径
static computeRootMatch(pathname) {
return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
}
constructor(props) {
console.log('router constructor');
super(props);
this.state = {
location: props.history.location, //挂载history的location属性
};
}
componentDidMount() {
this.unlisten = this.props.history.listen((location) => {
this.setState({ location });
});
}
componentWillUnmount() {
if (this.unlisten) {
this.unlisten();
}
}
render() {
//传递两个context给子组件
//一个是路由相关属性,包括history、location、match(是否匹配根路由)
//一个是history信息,同时将子组件渲染出来
console.log('render');
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
}}
>
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>
);
}
}
export default Router;
Router路由器组件,用于监听histroy的变更,并通过两个Context向子孙组件提供相关对象。通过Context的使用,当路由变更的时候可以触发React的更新机制,来重新渲染匹配的路由。
Route
import React, { Component } from 'react';
import RouterContext from './RouterContext.js';
const matchPath = (pathName, path) => {
return pathName === path;
};
/**
* 消费RouterContext,判断是否匹配路由,匹配则根据children、component、render
* 这三个prop情况渲染路由组件
* 优先级:children=>component=>render
*/
class Route extends Component {
render() {
return (
<RouterContext.Consumer>
{(context) => {
const location = this.props.location || context.location;
const match = matchPath(location.pathname, this.props.path);
const props = { ...context, location, match };
let { children, component, render } = this.props;
if (Array.isArray(children) && children.length === 0) {
children = null;
}
return (
<RouterContext.Provider value={props}>
{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;
Route路由组件,主要用于消费Router提供RouterContext,比较当前的location和path pros来是否需要渲染相关组件。渲染优先级:children=>component=>render
hook:useHistory
import React from 'react';
import HistoryContext from './HistoryContext';
export function useHistory() {
return React.useContext(HistoryContext);
}
useHistory的实现很简单,通过使用React.useContext直接读取先前定义的HistoryContext。就能获取确定的history对象。
总结
以上的相关代码只是选择了react-router库相关源码,实现其基本的路由功能。
其基本思路是:
- 定义路由操作接口,和订阅发布方式
- 路由器组件订阅路由更新
- 路由变更通知订阅对象,更新相关状态
- 触发重新渲染,匹配相关路由
通过mini-react-router,我们能够知道react-router 的组成和实现的基本思路。再遇到react-router相关问题,起码可以做到**"手上有粮,心中不慌"**。