router做为当前盛行的单页面应用必不可少的部分,今天咱们就以React-Router V4为例,来解开他的神秘面纱。本文并不专一于讲解 Reacr-Router V4 的基础概念,能够前往官方文档了解更多基础知识java
本文以RRV4代指Reacr-Router V4react
RRV4依赖的history库git
Q1. 为何咱们有时看到的写法是这样的github
import {
Switch,
Route, Router,
BrowserRouter, Link
} from 'react-router-dom';
复制代码
或是这样的?web
import {Switch, Route, Router} from 'react-router';
import {BrowserRouter, Link} from 'react-router-dom';
复制代码
react-router-dom和react-router有什么关系和区别?数组
Q2. 为何v4版本中支持div等标签的嵌套了?缓存
Q3. Route 会在当前 url 与 path 属性值相符的时候渲染相关组件,他是如何作到的呢?bash
Q4. 为何用Link组件,而不是a标签?服务器
进入RR V4以前,先想一想路由的做用,路由的做用就是同步url与其对应的回调函数。通常基于history,经过history.pushstate和replacestate方法修改url,经过window.addEventListener('popstate', callback) 来监听前进后退,对于hash路由,经过window.location.hash修改hash,经过window.addEventListener('hashchange', callback) 监听变化react-router
为了便于理解原理, 先来一段关于RRV4的简单代码
<BrowserRouter>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
</ul>
<hr/>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</div>
</BrowserRouter>
复制代码
在看看Route的子组件的props
{
match: {
path: "/", // 用来匹配的 path
url: "/", // 当前的 URL
params: {}, // 路径中的参数
isExact:true // 是否为严格匹配 pathname === "/"
},
location: {
hash: "" // hash
key: "nyi4ea" // 惟一的key
pathname: "/" // URL 中路径部分
search: "" // URL 参数
state: undefined // 路由跳转时传递的参数 state
}
history: {...} // history库提供
staticContext: undefined // 用于服务端渲染
}
复制代码
咱们带着问题去看源码,发现react-router-dom基于react-router,Router, Route, Switch等都是引用的react-router,而且加入了Link,BrowserRouter,HashRouter组件,这里解释了Q1,react-router负责通用的路由管理, react-router-dom负责web,固然还有react-router-native负责rn的管理,咱们从BrowserRouter开始看
rrv4的做者提倡Just Components 概念,BrowserRouter很简单,以组件的形式包装了Router,history传递下去,固然HashRouter也是同理
BrowserRouter源码
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} />;
}
}
复制代码
看完BrowserRouter,其实本质就是Router组件嘛,在下已经忍不住先去看react-router的Router源码了。
Router做为Route的根组件,负责监听url的变化和传递数据(props), 这里使用了history.listen监听url,使用react context的Provider和Consumer模式,最初的数据来自history,并将 history, location, match, staticContext做为props传递
router源码+注释
// 构造props
function getContext(props, state) {
return {
history: props.history,
location: state.location,
match: Router.computeRootMatch(state.location.pathname),
staticContext: props.staticContext
};
}
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
this.state = {
// browserRouter的props为history
location: props.history.location
};
this._isMounted = false;
this._pendingLocation = null;
// staticContext为true时,为服务器端渲染
// staticContext为false
if (!props.staticContext) {
// 监听listen,location改变触发
this.unlisten = props.history.listen(location => {
// _isMounted为true表示经历过didmount,能够setState,防止在构造函数中setstate
if (this._isMounted) {
// 更新state location
this.setState({ location });
} else {
// 不然存储到_pendingLocation, 等到didmount再setState避免可能报错
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
// 赋值为true,且不会再改变
this._isMounted = true;
// 更新location
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
// 取消监听
if (this.unlisten) this.unlisten();
}
render() {
const context = getContext(this.props, this.state);
return (
<RouterContext.Provider
children={this.props.children || null}
value={context}
/>
);
}
}
复制代码
rrv4中Router组件为context中的Pirover, children能够是任何div等元素,这里解释了问题Q2,react-router的v4版本直接推翻了以前的v2,v3版本,在v2,v3的版本中Router组件根据子组件的Route,生成全局的路由表,路由表中记录了path与UI组件的映射关系,Router监听path变化,当path变化时,根据新的path找出对应所需的全部UI组件,按必定层级将这些UI渲染出来.而在rrv4中做者提倡Just Components思想,这也符合react中一切皆组件的思想。
在v4中,Route只是一个Consumer包装的react组件,无论path是什么,Route组件总会渲染,在Route内部判断请求路径与当前的path是否匹配,匹配会继续渲染Route中的children或者component或者render中的子组件,若是不匹配,渲染null
route源码+注释
function getContext(props, context) {
const location = props.location || context.location;
const match = props.computedMatch
? props.computedMatch // <Switch> already computed the match for us
: props.path // <Route path='/xx' ... >
? matchPath(location.pathname, props)
: context.match; // 默认 { path: "/", url: "/", params: {}, isExact: pathname === "/" }
return { ...context, location, match };
}
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
// 经过path生成props
// this.props = {exact, path, component, children, render, computedMatch, ...others }
// context = { history, location, staticContext, match }
const props = getContext(this.props, context);
// 结构Route的props
let { children, component, render } = this.props;
// 空数组用null代替
if (Array.isArray(children) && children.length === 0) {
children = null;
}
if (typeof children === "function") {
// 无状态组件时
children = children(props);
if (children === undefined) {
children = null;
}
}
return (
<RouterContext.Provider value={props}>
{children && !isEmptyChildren(children) // children && React.Children.count > 0
? children
: props.match // match为true,查找到了匹配的<Route ... >
? component
? React.createElement(component, props) //建立react组件,传递props{ ...context, location, match }
: render
? render(props) // 执行render方法
: null
: null}
</RouterContext.Provider>
// 优先级 children > component > render
);
}}
</RouterContext.Consumer>
);
}
}
复制代码
Route是一个组件,每个Route都会监听本身context并执行从新的渲染,为子组件提供了新的props, props.match用来决定是否渲染component和render,props.match由matchPath生成, 这里咱们不得不看一下matchPath这个很重要的方法,他决定当前Route的path与url的匹配。
matchPath方法依赖path-to-regexp库, 举个小🌰
var pathToRegexp = require('path-to-regexp')
var keys = []
var re = pathToRegexp('/foo/:bar', keys)
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
复制代码
matchPath源码+注释
/** * Public API for matching a URL pathname to a path. * @param {*} pathname history.location.pathname * @param {*} options * 默认配置,是否全局匹配 exact,末尾加/ strict, 大小写 sensitive, path <Route path="/xx" ...> */
function matchPath(pathname, options = {}) {
// use <Switch />, options = location.pathname
if (typeof options === "string") options = { path: options };
const { path, exact = false, strict = false, sensitive = false } = options;
// path存入paths数组
const paths = [].concat(path);
return paths.reduce((matched, path) => {
if (matched) return matched;
// compilePath内部使用path-to-regexp库,并作了缓存处理
const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
// 在pathname中查找path
const match = regexp.exec(pathname);
// 匹配失败
if (!match) return null;
// 定义查找到的path为url
const [url, ...values] = match;
// 判断pathname与url是否相等 eg: '/' === '/home'
const isExact = pathname === url;
// 精准匹配时, 保证查找到的url === pathname
if (exact && !isExact) return null;
// 返回match object
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);
}
复制代码
不知道看官老爷有没有注意到这里
这就是Switch组件渲染与位置匹配的第一个子组件Route或Redirect的缘由。Switch利用React.Children.forEach(this.props.children, child => {...})方法匹配第一个子组件, 若是匹配成功添加computedMatch props,props值为match。从而改变了matchPath的逻辑
switch部分源码
class Switch extends React.Component {
render() {
...省略无关代码
let element, match;
React.Children.forEach(this.props.children, child => {
// child为react elemnet
// match若是没有匹配到这为context.match
if (match == null && React.isValidElement(child)) {
element = child;
// form用于<redirect form="..." ... >
const path = child.props.path || child.props.from;
// 匹配的match
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match; // path undefind为默认mactch
// note: path为undefined 时,会默认为'/'
}
});
return match // 添加computedMatch props为match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}
}
....
复制代码
到这里咱们了解了RRV4的基本工做流程和源码,解决了Q3,最后来看一下Link。
Link组件的主要用于处理理用户经过点击锚标签进行跳转,之因此不用a标签是由于要避免每次用户切换路由时都进行页面的总体刷新,而是使用histoy库中的push和replace。解决Q4,当点击Link组件时,点击的是页面上渲染出来的 a 标签,经过preventDefault阻止默认行为,经过history的push或repalce跳转。
Link部分源码+注释
class Link extends React.Component {
....
handleClick(event, context) {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // 忽略不是左键的点击
(!this.props.target || this.props.target === "_self") && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
// 阻止默认行为
event.preventDefault();
const method = this.props.replace
? context.history.replace
: context.history.push;
method(this.props.to);
}
...
render() {
....
return(
<a {...rest} onClick={event => this.handleClick(event, context)} href={href} ref={innerRef} /> ) } '''' } 复制代码
到这里,本文结束,欢迎看官老爷们的到来。