目录
前言
此篇文章默认读者已经了解react-router
的api
使用方法;
在看这篇文章之前, 需要先对react-router
和react-router-dom
有一个简单的了解;
首先来看官方对两者的描述
The core of React Router (react-router)DOM bindings for React Router (react-router-dom)
react-router
是React Router
的核心, 实现了路由的核心功能;
react-router-dom
是React Router
的DOM
绑定, 提供了浏览器环境下的功能, 比如Link
, BrowserRouter
等组件;
可以理解为, react-router-dom
基于react-router
, 安装依赖的时候只需要安装react-router-dom
就好了!
react-router结构分析
根据官方文档, 使用react-router-dom
进行路由管理, 首先我们选择一个路由模式:
- BrowserRouter:
History
模式 - HashRouter:
Hash
模式 - MemoryRouter: 在没有
url
的情况下, 使用Memory
记住路由, 常见在React Native
中使用, 这里不进行讨论
以下都以create-react-app
为例
这里选择History
模式, 也就是在最外层使用BrowserRouter
组件:
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter as Router} from 'react-router-dom';
import App from './App';
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById('root')
);
然后在被BrowserHistory
组件包裹的组件中可以使用Route
进行路由划分:
App.tsx
import React from 'react';
import {Route} from 'react-router-dom';
const Page1.React.FC = props => {
return <div>Page1</div>;
};
const Page2.React.FC = props => {
return <div>Page2</div>;
};
function App() {
return (
<div className="App">
<Route path="/page1" component={Page1}></Route>
<Route path="/page2" component={Page2}></Route>
</div>
);
}
export default App;
以上就是react-router
的大概结构, 下面将对react-router-dom
的组件进行源码分析
BrowserHistory
BrowserHistory
和HashHistory
的代码结构和逻辑相似, 这里只对BrowserHistory
作分析;
BrowserHistory
核心代码逻辑分析:
定义BrowserHistory
传入的prop
类型
import PropTypes from "prop-types";
class BrowserRouter extends React.Component {
// 此处代码略去
}
BrowserRouter.propTypes = {
basename: PropTypes.string,
children: PropTypes.node,
forceRefresh: PropTypes.bool,
getUserConfirmation: PropTypes.func,
keyLength: PropTypes.number
};
使用history
的createBrowserHistory
, 将props
作为参数, 创建一个history
实例, 并将history
传入Router
组件中
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
从源码中可以看出, BrowserHistory
只是对Router
组件的简单包装;
Router
react-router-dom
中的Router
实际上就是react-router
的Router
, 此处直接对react-router
的Router
进行源码分析:
定义Router
传入的props
类型:
import PropTypes from "prop-types";
Router.propTypes = {
children: PropTypes.node,
history: PropTypes.object.isRequired,
staticContext: PropTypes.object
};
staticContext
是staticRouter
中传入Router
的属性, 源码中所有使用了props.staticContext
的代码都不做讨论;
Router
的构造函数中,声明this.state.location
, 使用history.listen
对history.location
进行监听, 并将history.listen
的返回值(用于移除监听事件)赋值给this.unlisten
, 在componentWillUnmount
生命周期中进行调用;
之所以在构造函数中就对history.location
进行监听, 而不是在componentDidMount
中进行监听, 官方是这么解释的:
This is a bit of a hack. We have to start listening for location changes here in the constructor in case there are any <Redirect>s on the initial render. If there are, they will replace/push when they mount and since cDM fires in children before parents, we may get a new location before the <Router> is mounted.
大概意思就是, 因为子组件会比父组件更早渲染完成, 并且因为Redirect
的存在, 若是在Router
的componentDidMount
中对history.location
进行监听, 则有可能在监听事件注册之前, history.location
已经由于Redirect
组件发生了多次改变, 因此我们需要在Router
的constructor
中就注册监听事件;
import React from 'react';
class Router extends React.Component {
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
// This is a bit of a hack. We have to start listening for location
// changes here in the constructor in case there are any <Redirect>s
// on the initial render. If there are, they will replace/push when
// they mount and since cDM fires in children before parents, we may
// get a new location before the <Router> is mounted.
this._isMounted = false;
this._pendingLocation = null;
if (!props.staticContext) { // props.staticContext不存在, 因此默认为true
this.unlisten = props.history.listen(location => {
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
if (this.unlisten) {
this.unlisten();
this._isMounted = false;
this._pendingLocation = null;
}
}
// 以下代码省略
}
react-router
中使用context
进行组件通信; 在Router
中, 使用RouterContext.Provider
进行router
数据(history
,location
, match
以及staticContext
)传递, 使用HistoryContext.Provider
进行history
数据传递, 子组件(Route
或是Redirect
等)可以通过RouterContext.Consumer
或是HistoryContext.Consumer
对上层数据进行接收; HistoryContext
和RouterContext
都是使用mini-create-react-context
的createContext
方法创建的context
, mini-create-react-context
工具库自身定义如下:
(A smaller) Polyfill for the React context API
mini-create-react-context
是React context API
的Polyfil
, 因此可以直接将mini-create-react-context
当成React context API
;
import React from "react";
import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
>
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>
);
}
}
Switch
<Switch>
is unique in that it renders a route exclusively
即使有多个路由组件成功匹配, Switch
也只展示一个路由;
<Switch>
必须作为<Router>
的子组件进行使用, 若是脱离<Router>
, 则会报错:
"You should not use <Switch> outside a <Router>"
定义Switch
中传入的props
类型:
import PropTypes from "prop-types";
Switch.propTypes = {
children: PropTypes.node,
location: PropTypes.object
};
使用RouterContext.Consumer
接收RouterContext.Provider
的路由信息; Switch
对路由组件进行顺序匹配, 使用React.Children.forEach
对Switch
子组件进行遍历, 每次遍历逻辑如下:
使用
React.isValidElement
判断子组件是否为有效的element
:- 有效: 则进入步骤二;
- 无效: 结束此轮循环, 进行下一轮循环;
声明
path:
const path = child.props.path || child.props.from;
注:
<Route>
使用path
进行路由地址声明,<Redirect>
使用from
进行重定向来源地址声明;接着判断
path
是否存在:- 存在
path
的情况下, 表示子组件存在路由映射关系, 使用matchPath
对path
进行匹配, 判断路由组件的路径与当前location.pathname
是否匹配: 若是匹配, 则对子组件进行渲染, 并将matchPath
返回的值作为computedMatch
传递到子组件中, 并且不再对其他组件进行渲染; 若是不匹配, 则直接进行下次循环; 注意:location
可以是外部传入的props.location
, 若是props.location
不存在, 则为context.location
; - 不存在
path
的情况下, 表示子组件不存在路由映射关系, 直接渲染该子组件, 并将context.match
作为computedMatch
传入子组件中;
- 存在
matchPath
是react-router
的一个api
, 源码中注释对matchPath
的介绍如下:
Public API for matching a URL pathname to a path.
主要用于匹配路由, 匹配成功则返回一个match
, 若是匹配失败, 则返回null
;
import React from 'react';
import RouterContext from "./RouterContext.js";
import matchPath from "./matchPath.js";
/**
* The public API for rendering the first <Route> that matches.
*/
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Switch> outside a <Router>");
const location = this.props.location || context.location;
let element, match;
// We use React.Children.forEach instead of React.Children.toArray().find()
// here because toArray adds keys to all child elements and we do not want
// to trigger an unmount/remount for two <Route>s that render the same
// component at different URLs.
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
Route
The Route component is perhaps the most important component in React Router to understand and learn to use well. Its most basic responsibility is to render some UI when its path
matches the current URL
<Route>
可能是react-router
中最重要的组件, 它最基本的职责是在其路径与当前URL匹配时呈现对应的UI组件;
与其他非Router
组件一样, 若是不被<RouterContext.Provider>
包裹, 则会报错:
"You should not use <Switch> outside a <Router>"
定义Route
的props
类型:
import PropTypes from "prop-types";
Route.propTypes = {
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
component: (props, propName) => {
if (props[propName] && !isValidElementType(props[propName])) {
return new Error(
`Invalid prop 'component' supplied to 'Route': the prop is not a valid React component`
);
}
},
exact: PropTypes.bool,
location: PropTypes.object,
path: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
]),
render: PropTypes.func,
sensitive: PropTypes.bool,
strict: PropTypes.bool
};
与其它路由组件一样, 使用RouterContext.Consumer
接收全局路由信息; 因为Route
的逻辑比较简单, 主要判断path
与当前路由是否匹配, 若是匹配则进行渲染对应路由组件, 若是不匹配则不进行渲染, 核心代码如下:
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
...
<RouterContext.Provider value={props}>
{
props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null
}
</RouterContext.Provider>
注: 根据上面代码, 不论props.match
是否为true
, 当Route
的children
为函数时都会进行渲染;
总结
本篇文章对react-router
的部分核心组件进行源码解读; react-router
使用Context.Provider
向路由树传递路由信息, Route
等组件通过Context.Consumer
接收路由信息, 匹配路径并渲染路由组件, 以及与上篇文章讲到的history
的紧密配合, 才让react-router
如此优秀; 下一篇文章将对剩余组件以及react-router
的hooks
进行源码解读!
如果发现文章有错误可以在评论区里留言哦, 欢迎指正!
上一篇文章: 从源码对react-router v5进行原理分析(一)
兴趣向文章: LogGame - 藏在浏览器控制台里的汽车游戏🚗, 云音乐用户信息可视化: 对网易云音乐用户的一次有趣的数据分析
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。