在我们使用react
这个库搭建前端工程的时候,是我们不可避免需要使用到第三方的路由库来划分各个模块,或者说页面,而主要的路由库就是react-router
。为了进一步了解这个库是如何实现前端路由的,需要对其源码有初步的了解。
通常对于路由库功能的理解,就是监听前端路由的改变,动态渲染匹配当前路由的组件。
搭建一个 mini 的react-router
,有助于理解这个库是如何实现前端路由功能的,对其实现的思路有基本了解。以及了解提供的 hook 之一useHistory
的功能和实现方法。至于其他react-router
的功能,在此基础上也能很好的理解。
源码:mini-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
相关问题,起码可以做到**"手上有粮,心中不慌"**。
评论