共计 8720 个字符,预计需要花费 22 分钟才能阅读完成。
目录
- 前言
- react-router 构造剖析
- BrowserHistory
- Router
- Switch
- Route
前言
此篇文章默认读者曾经理解 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 – 藏在浏览器控制台里的汽车游戏????, 云音乐用户信息可视化: 对网易云音乐用户的一次乏味的数据分析