react-router 是 React 官方维护的一个路由库,它通过管理 URL,实现组件的切换和状态的变化。
react-router 包含3个库,react-router、react-router-dom、react-router-native。react-router 提供最基本的路由功能,在实际使用中我们不会直接安装 react-router,而是根据应用运行的环境选择安装 react-router-dom (在浏览器中使用) 或 react-router-native (在 rn 中使用)。react-router-dom和 react-router-native都依赖react-router,所以在安装时,react-router也会自动安装。
react-router-dom
react-router-dom是React Router的DOM绑定, 提供了浏览器环境下的功能, 比如<Link>, <BrowserRouter>等组件;
下面,我们来实现一个简单版的 react-router-dom:
RouterContext 组件实现
import React from 'react';
export const RouterContext = React.createContext();
Router 组件实现
在 Router 的构造函数中,声明 this.state.location,然后使用 history 的监听函数 listen 对 history.location 进行监听,并将 history.listen 的返回值赋值给 this.unlisten,用于取消监听。
在 中, 使用 <RouterContext.Provider> 进行路由数据传递(history,location, match)。
import React, { Component } from 'react';
import { RouterContext } from './Context';
export default class Router extends Component {
constructor(props) {
super(props);
this.state = {
location: props.history.location
}
// 监听 location 变化
this.unlisten = props.history.listen(location => this.setState({location})
}
static computeRootMatch(pathname) {
return { path: '/', url: '/', params: {}, isExact: pathname === '/' }
}
componentWillUnMount() {
// 取消监听
if (this.unlisten) this.unlisten()
}
render() {
return (
// 使用 <RouterContext.Provider> 进行路由数据传递
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname)
}}
>
{this.props.children} // 渲染<Router>的子组件内容
</RouterContext.Provider>
)
}
}
BrowserRouter 组件实现
在 中,使用 history 的 createBrowserHistory 方法,将 props 作为参数,创建一个 history 实例,并将 history 传入 Router 组件中。
import React, { Component } form 'react';
import { createBrowserHistory } from 'history';
import Router from './Router';
export default class BrowserRouter extends Component {
constructor(props) {
super(props);
this.history = createBrowserHistory();
}
render() {
return <Router history={this.history}>{this.props.children}</Router>
}
}
HashRouter 组件实现
import React, { Component, Children } from 'react';
import { createHashHistory } from 'history';
import { RouterContext } from './RouterContext';
import Router from './Router';
export default class HashRouter extends Component {
constructor(props) {
super(props);
this.history = createHashHistory();
}
render() {
return <Router history={this.history} children={this.props.children} />
}
}
MemoryRouter 组件实现
把 URL 的历史记录保存在内存中的 (不读取,不写入地址栏)。在测试和非浏览器环境中非常有用,如 React Native.
import React, { Component, Children } from 'react';
import { createMemoryHistory } from 'history';
import { RouterContext } from './RouterContext';
import Router from './Router';
export default class MemoryRouter extends Component {
constructor(props) {
super(props);
this.history = createMemoryHistory();
}
render() {
return <Router history={this.history} children={this.props.children} />
}
}
Route 组件实现
可能是 react-router 中最重要的组件,其职责是在路径与当前 URL 匹配时渲染对应的UI组件。
与其他路由组件一样,使用 <RouterContext.Consumer> 接收全局路由信息。使用 <RouterContext.Provider> 进行路由数据传递。
import React, { Component } from 'react';
import { RouterContext } from './Context';
import matchPath from './matchPath';
export default class Route extends Component {
render() {
return (
<RouterContext.Consumer>
{
context => {
const location = context.location;
const { path, children, component, render } = this.props;
const match = this.props.computedMatch
? this.props.computedMatch
: path
? matchPath(location.pathname, this.props)
: context.match;
const props = {
...context,
match
}
return (
// 使用 <RouterContext.Provider> 进行路由数据传递
<RouterContext.Provider value={props}>
{
match
? children // 渲染方式为 children
? typeof children === "function"
? children(props) // 传递给 children 的是一个 function,执行该function
: children // 传递给 children的 是节点元素
: component // 渲染方式为 component
? React.createElement(component, props) // 创建一个新的 React Element
: render // 渲染方式为 render
? render(props)
: null // Route 组件的三种渲染方式都没有时
: typeof children === "function"
? children(props) // 路由不匹配时,仅渲染children 为 function的情形
: null
}
</RouterContext.Provider>
)
}
}
</RouterContext.Consumer>
)
}
}
的渲染方式有三种,分别是 children、component、render。由上面的代码可知,这三种渲染方式是互斥。每次只能用其中的一种渲染方式。三者的渲染优先级是 children > component > render。
无论 props.match 是否为 true,当 的 children 为 函数时都会进行渲染。
matchPath 方法实现
主要用于匹配路由, 匹配成功则返回一个match对象, 若是匹配失败, 则返回null;
import pathToRegexp from "path-to-regexp";
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;
function compilePath(path, options) {
const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
if (pathCache[path]) return pathCache[path];
const keys = [];
const regexp = pathToRegexp(path, keys, options);
const result = { regexp, keys };
if (cacheCount < cacheLimit) {
pathCache[path] = result;
cacheCount++;
}
return result;
}
/**
* Public API for matching a URL pathname to a path.
*/
function matchPath(pathname, options = {}) {
if (typeof options === "string" || Array.isArray(options)) {
options = { path: options };
}
const { path, exact = false, strict = false, sensitive = false } = options;
const paths = [].concat(path);
return paths.reduce((matched, path) => {
if (!path && path !== "") return null;
if (matched) return matched;
const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
const match = regexp.exec(pathname);
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) return null;
return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}
export default matchPath;
Link 组件实现
实现了路由的跳转;import React, { Component } from 'react';
import { RouterContext } from './Context';
export default class Link extends Component {
static contextType = RouterContext;
handleClick = e => {
e.preventDefault();
this.context.history.push(this.props.to);
};
render() {
const { children, to, ...restProps } = this.props;
return (
<a href={to} {...restProps} onClick={this.handleClick}>
{ children }
</a>
)
}
}
Switch 组件实现
使用 <RouterContext.Consumer> 进行数据接收; 对路由组件( 或者是 ) 进行顺序匹配,找到第一个匹配的 或者 。
使用 React.Children.forEach 方法 对 的子组件进行遍历,其遍历逻辑如下:
首先使用 React.isValidElement 判断子组件是否为有效的 element:
- 无效,则结束当前循环,进行下一轮循环
- 有效,则获取通过 child.props.path 或者 child.props.from 获取 path
PS: 使用 path 进行路由地址声明, 使用 from 进行重定向来源地址声明
获取 path 之后,接着判断 path 是否存在:
- 若存在path: 表示子组件存在路由映射关系, 使用matchPath对path进行匹配, 判断路由组件的路径与当前
location.pathname是否匹配: - 若是匹配, 则对子组件进行渲染, 并将matchPath返回的值作为
computedMatch传递到子组件中, 并且不再对其他组件进行渲染; - 若是不匹配, 则直接进行下次循环; 注意:
location可以是外部传入的props.location, 默认为context.location; - 若不存在path: 表示子组件不存在路由映射关系, 直接渲染该子组件, 并将
context.match作为computedMatch传入子组件中;
import React, { Component } from 'react';
import { RouterContext } from './Context';
import matchPath from './matchPath';
export default class Switch extends Component {
render() {
return (
<RouterContext.Consumer>
{
context => {
const { location } = context;
let match = undefined; // 匹配的 match
let element = undefined; // 匹配的元素
React.Children.forEach(this.props.children, child => {
// child 是 Route 或 Redirect
if (match == null && React.isValidElement(child)) {
element = child;
// <Route> 使用 path 进行路由地址声明,<Redirect> 使用 from 进行重定向来源地址声明
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, child.props)
: context.match;
}
});
return match
? React.cloneElement(element, {computedMatch: match})
: null;
}
}
</RouterContext.Consumer>
)
}
}
withRouter 高阶组件实现
是一个高阶组件,支持传入一个组件,返回一个能访问路由数据的路由组件,实际上是将组件作为 <RouterContext.Consumer> 的子组件,并将 context 的路由信息作为 props 注入组件中。
import React, { Component } from './react';
import { RouterContext } from './Context';
// withRouter 是一个高阶组件
const withRouter = WrappedComponent => props => {
return (
<RouterContext.Consumer>
{
context => <WrappedComponent {...props} { ...context } />
}
</RouterContext.Consumer>
)
}
export default withRouter;
Redirect 组件实现
使用 接收路由数据。
通过传入的 push 确定其跳转方式是 push 还是 replace。
import React, { Component } from 'react';
import { RouterContext } from './Context';
import LifeCycle from './LifeCycle';
export default Class Redirect extends Component {
render() {
return (
<RouterContext.Consumer>
{
context => {
const { to, push=false } = this.props;
return (
<LifeCycle
onMount={() => {
// 通过 传入的 push 确定跳转方式是 push 还是 replace
push ? context.history.push(to) : context.history.replace(to);
}}
/>
)
}
}
</RouterContext.Consumer>
)
}
}
Prompt 组件实现
import React from 'react';
import { RouterContext } from './Context';
import LifeCycle from './LifeCycle';
export default function Prompt({ message, when=true }) {
return (
<RouterContext.Consumer>
{
context => {
if (!when) return null;
let method = context.history.block;
return (
<LifeCycle
onMount={ self => self.replace = method(message)}
onUnMount={ self => self.release() }
/>
)
}
}
</RouterContext.Consumer>
)
}
LIfeCycle 组件实现
组件支持传入的 onMount、onUpdate 及 onUnMount 三个方法,分贝代表着 componentDidMount、componentDidUpdate、componentWillUnMount
import React, { Component } from 'react';
export default class LifeCycle extends Component {
componentDidMount() {
if (this.props.onMount) this.props.onMount.call(this, this);
}
componentWillUnmount() {
if (this.props.onUnMount) this.props.onUnMount.call(this, this);
}
componentDidUpdate(prevProps) {
if (this.props.onUpdate) this.props.onUpdate.call(this, this, prevProps);
}
render() {
return null;
}
}
hooks 实现
hooks 可以让我们在组件中获取到路由的状态并且执行导航。如果需要使用 hooks,使用的React版本应该在16.8以上。
hooks 利用 React 提供的hooks: useContext,让我们可以在组件中访问到 RouterContext 中的数据。
hooks.js
import {RouterContext} from "./Context";
import {useContext} from "react";
import matchPath from "./matchPath";
export function useHistory() {
return useContext(RouterContext).history;
}
export function useLocation() {
return useContext(RouterContext).location;
}
export function useRouteMatch() {
return useContext(RouterContext).match;
}
export function useParams() {
const match = useContext(RouterContext).match;
return match ? match.params : {};
}
index.js 实现
index.js
import BrowserRouter from "./BrowserRouter";
import Route from "./Route";
import Link from "./Link";
import Switch from "./Switch";
import {useRouteMatch, useHistory, useLocation, useParams} from "./hooks";
import withRouter from "./withRouter";
import Prompt from "./Prompt";
import Redirect from "./Redirect";
export {
BrowserRouter,
Route,
Link,
Switch,
useRouteMatch,
useHistory,
useLocation,
useParams,
withRouter,
Prompt,
Redirect,
};