回顾:上一篇讲了BrowserRouter 和 Router之前的关系,以及Router实现路由跳转切换的原理。这一篇来简短介绍react-router剩余组件的源码,结合官方文档,一起探究实现的的方式。
1. Switch.js
Switch对props.chidlren做遍历筛选,将第一个与pathname匹配到的Route或者Redirect进行渲染(此处只要包含有path这个属性的子节点都会进行筛选,所以可以直接使用自定义的组件,如果缺省path这个属性,并且当匹配到这个子节点时,那么这个子节点就会被渲染同时筛选结束,即Switch里任何时刻只渲染唯一一个子节点),当循环结束时仍没有匹配到的子节点返回null。Switch接收两个参数分别是:
- ①:location, 开发者可以填入location参数来替换地址栏中的实际地址进行匹配。
- ②:children,子节点。
源码如下:
import React from "react";
import PropTypes from "prop-types";
import warning from "warning";
import invariant from "invariant";
import matchPath from "./matchPath";
class Switch extends React.Component {
// 接收Router组件传递的context api,这也是为什么Switch要写在
// Router内部的原因
static contextTypes = {
router: PropTypes.shape({
route: PropTypes.object.isRequired
}).isRequired
};
static propTypes = {
children: PropTypes.node,
location: PropTypes.object
};
componentWillMount() {
invariant(
this.context.router,
"You should not use <Switch> outside a <Router>"
);
}
componentWillReceiveProps(nextProps) {
// 这里的两个警告是说,对于Switch的location这个参数,我们不能做如下两种操作
// 从无到有和从有到无,猜测这样做的原因是Switch作为一个渲染控制容器组件,在每次
// 渲染匹配时要做到前后的统一性,即不能第一次使用了地址栏的路径进行匹配,第二次
// 又使用开发者自定义的pathname就行匹配
warning(
!(nextProps.location && !this.props.location),
'<Switch> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
);
warning(
!(!nextProps.location && this.props.location),
'<Switch> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
);
}
render() {
// Router提供的api,包括history对象,route对象等。route对象包含两个参数
// 1.location:history.location,即在上一章节里讲到的history这个库
// 根据地址栏的pathname,hash,search,等创建的一个location对象。
// 2.match 就是Router组件内部的state, 即{path: '/', url: '/', params: {}, isEaxct: true/false}
const { route } = this.context.router;
const { children } = this.props; // 子节点
// 自定义的location或者Router传递的location
const location = this.props.location || route.location;
// 对所有子节点进行循环操作,定义了mactch对象来接收匹配到
// 的节点{path,url,parmas,isExact}等信息,当子节点没有path这个属性的时候
// 且子节点被匹配到,那么这个match会直接使用Router组件传递的match
// child就是匹配到子节点
let match, child;
React.Children.forEach(children, element => {
// 判断子节点是否是一个有效的React节点
// 只有当match为null的时候才会进入匹配的操作,初看的时候感觉有些奇怪
// 这里主要是matchPath这个方法做了什么?会在下一节讲到,这里只需要知道
// matchPath接收了pathname, options={path, exact...},route.match等参数
// 使用正则库判断path是否匹配pathname,如果匹配则会返回新的macth对象,
// 否则返回null,进入下一次的循环匹配,巧妙如斯
if (match == null && React.isValidElement(element)) {
const {
path: pathProp,
exact,
strict,
sensitive,
from
} = element.props; // 从子节点中获取props信息,主要是pathProp这个属性
// 当pathProp不存在时,使用替代的from,否则就是undefined
// 这里的from参数来自Redirect,即也可以对redirect进行校验,来判断是否渲染redirect
const path = pathProp || from;
child = element;
match = matchPath(
location.pathname,
{ path, exact, strict, sensitive },
route.match
);
}
});
// 如果match对象匹配到了,则调用cloneElement对匹配到child子节点进行clone
// 操作,并传递了两个参数给子节点,location对象,当前的地址信息
// computedMatch对象,匹配到的路由参数信息。
return match
? React.cloneElement(child, { location, computedMatch: match })
: null;
}
}
export default Switch;
2. matchPath.js
mathPath是react-router用来将path生成正则对象并对pathname进行匹配的一个功能方法。当path不存在时,会直接返回Router的match结果,即当子组件的path不存在时表示该子组件一定会被选渲染(在Switch中如果子节点没有path,并不一定会被渲染,还需要考虑节点被渲染之前不能匹配到其他子节点)。matchPath依赖一个第三方库path-to-regexp,这个库可以将传递的options:path, exact, strict, sensitive 生成一个正则表达式,然后对传递的pathname进行匹配,并返回匹配的结果,服务于Switch,Route组件。参数如下:
- ① :pathname, 真实的将要被匹配的路径地址,通常这个地址是地址栏中的pathname,开发者也可以自定义传递location对象进行替换。
- ②:options,用来生成pattern的参数集合:
path: string, 生成正则当中的路径,比如“/user/:id”,非必填项无默认值
exact: false,默认值false。即使用正则匹配到结果url和pathname是否完全相等,如果传递设置为true,两者必须完全相等才会返回macth结果
strict: false,默认值false。即pathname的末尾斜杠会不会加入匹配规则,正常情况下这个参数用到的不多。
sensitive: false, 默认值false。即正则表达式是否对大小写敏感,同样用到的不多,不过某些特殊场景下可能会用到。
源码如下:
import pathToRegexp from "path-to-regexp";
// 用来缓存生成过的路径的正则表达式,如果遇到相同配置规则且相同路径的缓存,那么直接使用缓存的正则对象
const patternCache = {};
const cacheLimit = 10000; // 缓存的最大数量
let cacheCount = 0; // 已经被缓存的个数
const compilePath = (pattern, options) => {
// cacheKey表示配置项的stringify序列化,使用这个作为patternCache的key
const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
// 每次先从patternCache中寻找符合当前配置项的缓存对象,如果对象不存在那么设置一个
const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
// 如果存在以 path 路径为key的对象,表示该路径被生成过,那么直接返回该正则信息
// 至于为什么要做成多层的key来缓存,即相同的配置项作为第一层key,pattern作为第二层key
// 应该是即便我们使用obj['xx']的方式来调用某个值,js内部依然是要进行遍历操作的,这样封装
// 两层key,是为了更好的做循环的优化处理,减少了遍历查找的时间。
if (cache[pattern]) return cache[pattern];
const keys = []; // 用来存储动态路由的参数key
const re = pathToRegexp(pattern, keys, options);
const compiledPattern = { re, keys }; //将要被返回的结果
// 当缓存数量小于10000时,继续缓存
if (cacheCount < cacheLimit) {
cache[pattern] = compiledPattern;
cacheCount++;
}
// 返回生成的正则表达式已经动态路由的参数
return compiledPattern;
};
/**
* Public API for matching a URL pathname to a path pattern.
*/
const matchPath = (pathname, options = {}, parent) => {
// options也可以直接传递一个path,其他参数方法会自动添加默认值
if (typeof options === "string") options = { path: options };
// 从options获取参数,不存在的参数使用默认值
const { path, exact = false, strict = false, sensitive = false } = options;
// 当path不存在时,直接返回parent,即父级的match匹配信息
if (path == null) return parent;
// 使用options的参数生成,这里将exact的参数名改为end,是因为path-to-regexp用end参数来表示
// 是否匹配完整的路径。即如果默认false的情况下,path: /one 和 pathname: /one/two,
// path是pathname的一部分,pathname包含了path,那么就会判断此次匹配成功
const { re, keys } = compilePath(path, { end: exact, strict, sensitive });
const match = re.exec(pathname); // 对pathname进行匹配
if (!match) return null; // 当match不存在时,表示没有匹配到,直接返回null
// 从match中获取匹配到的结果,以一个path-to-regexp的官方例子来表示
// const keys = []
// const regexp = pathToRegexp('/:foo/:bar', keys)
// regexp.exec('/test/route')
//=> [ '/test/route', 'test', 'route', index: 0, input: '/test/route', groups: undefined ]
const [url, ...values] = match;
const isExact = pathname === url; // 判断是否完全匹配
if (exact && !isExact) return null; // 当exact值为true且没有完全匹配时返回null
return {
path, // the path pattern 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) => {
// 获取动态路由的参数,即传递的path: '/:user/:id', pathname: '/xiaohong/23',
// params最后返回的结果就是 {user: xiaohong, id: 23}
memo[key.name] = values[index];
return memo;
}, {})
};
};
export default matchPath;
简单介绍一下path-to-regexp的用法,path-to-regexp的官方地址:链接描述
const pathToRegexp = require('path-to-regexp')
const keys = []
const regexp = pathToRegexp('/foo/:bar', keys)
// regexp = /^\/foo\/([^\/]+?)\/?$/i 表示生成的正则表达式
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
// keys表示动态路由的参数信息
regexp.exec('/test/route') // 对pathname进行匹配并返回匹配的结果
//=> [ '/test/route', 'test', 'route', index: 0, input: '/test/route', groups: undefined ]
3. Route.js
Route.js 是react-router最核心的组件,通过对path进行匹配,来判断是否需要渲染当前组件,它本身也是一个容器组件。细节上需要注意的是,只要path被匹配那么组件就会被渲染,并且Route组件在非Switch包裹的前提下,不受其他组件渲染的影响。当path参数不存在的时候,组件一定会被渲染。
源码如下:
import warning from "warning";
import invariant from "invariant";
import React from "react";
import PropTypes from "prop-types";
import matchPath from "./matchPath";
// 判断children是否为空
const isEmptyChildren = children => React.Children.count(children) === 0;
class Route extends React.Component {
static propTypes = {
computedMatch: PropTypes.object, // 当外部使用Switch组件包裹时,此参数由Switch传递进来表示当前组件被匹配的信息
path: PropTypes.string,
exact: PropTypes.bool,
strict: PropTypes.bool,
sensitive: PropTypes.bool,
component: PropTypes.func, // 组件
render: PropTypes.func, // 一个渲染函数,函数的返回结果为一个组件或者null,一般用来做鉴权操作
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), // props.children, 子节点
location: PropTypes.object //自定义的location信息
};
// 接收Router组件传递的context api
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.object.isRequired,
route: PropTypes.object.isRequired,
staticContext: PropTypes.object // 由staticRouter传递,服务端渲染时会用到
})
};
// 传递给子组件的 context api
static childContextTypes = {
router: PropTypes.object.isRequired
};
// Router组件中也有类似的一套操作,不同的是将Router传递的match进行了替换,而
// location对象如果当前传递了自定义的location,也就会被替换,否则还是Router组件中传递过来的location
getChildContext() {
return {
router: {
...this.context.router,
route: {
location: this.props.location || this.context.router.route.location,
match: this.state.match
}
}
};
}
// 返回当前Route传递的options匹配的信息,匹配过程请看matchPath方法
state = {
match: this.computeMatch(this.props, this.context.router)
};
computeMatch(
{ computedMatch, location, path, strict, exact, sensitive },
router
) {
// 特殊情况,当有computeMatch这个参数的时候,表示当前组件是由上层Switch组件
// 已经进行渲染过后进行clone的组件,那么直接进行渲染不需要再进行匹配了
if (computedMatch) return computedMatch;
invariant(
router,
"You should not use <Route> or withRouter() outside a <Router>"
);
const { route } = router; //获取Router组件传递的route信息,即包括location、match两个对象
const pathname = (location || route.location).pathname;
// 返回matchPath匹配的结果
return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
}
componentWillMount() {
// 当同时传递了component 和 render两个props,那么render将会被忽略
warning(
!(this.props.component && this.props.render),
"You should not use <Route component> and <Route render> in the same route; <Route render> will be ignored"
);
// 当同时传递了 component 和 children并且children非空,会进行提示
// 并且 children 会被忽略
warning(
!(
this.props.component &&
this.props.children &&
!isEmptyChildren(this.props.children)
),
"You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored"
);
// 当同时传递了 render 和 children并且children非空,会进行提示
// 并且 children 会被忽略
warning(
!(
this.props.render &&
this.props.children &&
!isEmptyChildren(this.props.children)
),
"You should not use <Route render> and <Route children> in the same route; <Route children> will be ignored"
);
}
// 不允许对Route组件的locatin参数 做增删操作,即Route组件应始终保持初始状态,
// 可以被Router控制,或者被开发者控制,一旦创建则不能进行更改
componentWillReceiveProps(nextProps, nextContext) {
warning(
!(nextProps.location && !this.props.location),
'<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
);
warning(
!(!nextProps.location && this.props.location),
'<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
);
// 这里看到并没有对nextProps和this.props做类似的比较,而是直接进行了setState来进行rerender
// 结合上一章节讲述的Router渲染的流程,顶层Router进行setState之后,那么所有子Route都需要进行
// 重新匹配,然后再渲染对应的节点数据
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
});
}
render() {
const { match } = this.state; // matchPath的结果
const { children, component, render } = this.props; //三种渲染方式
const { history, route, staticContext } = this.context.router; // context router api
const location = this.props.location || route.location; // 开发者自定义的location优先级高
const props = { match, location, history, staticContext }; // 传递给子节点的props数据
// component优先级最高
if (component) return match ? React.createElement(component, props) : null;
// render优先级第二,返回render执行后的结果
if (render) return match ? render(props) : null;
// 如果children是一个函数,那么返回执行后的结果 与render类似
// 此处需要注意即children是不需要进行match验证的,即只要Route内部
// 嵌套了节点,那么只要不同时存在component或者render,这个内部节点一定会被渲染
if (typeof children === "function") return children(props);
// Route内的节点为非空,那么保证当前children有一个包裹的顶层节点才渲染
if (children && !isEmptyChildren(children))
return React.Children.only(children);
// 否则渲染一个空节点
return null;
}
}
export default Route;
4. withRouter.js
withRouter.js 作为react-router中的唯一HOC,负责给非Route组件传递context api,即 router: { history, route: {location, match}}。它本身是一个高阶组件,并使用了
hoist-non-react-statics这个依赖库,来保证传递的组件的静态属性。
高阶组件的另外一个问题就是refs属性,引用官方文档的解释:虽然高阶组件的约定是将所有道具传递给包装组件,但这对于refs不起作用,是因为ref不是真正的prop,它是由react专门处理的。如果将添加到当前组件,并且当前组件由hoc包裹,那么ref将引用最外层hoc包装组件的实例而并非我们期望的当前组件,这也是在实际开发中为什么不推荐使用refs string的原因,使用一个回调函数是一个不错的选择,withRouter也同样的使用的是回调函数来实现的。react官方推荐的解决方案是 React.forwardRef API(16.3版本), 地址如下:链接描述
源码如下:
import React from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";
import Route from "./Route";
// withRouter使用的也是Route容器组件,这样Component就可以直接使用props获取到history等api
const withRouter = Component => {
// withRouter使用一个无状态组件
const C = props => {
// 接收 wrappedComponentRef属性来返回refs,remainingProps保留其他props
const { wrappedComponentRef, ...remainingProps } = props;
// 实际返回的是Componetn由Route组件包装的, 并且没有path等属性保证Component组件一定会被渲染
return (
<Route
children={routeComponentProps => (
<Component
{...remainingProps} // 直接传递的其他属性
{...routeComponentProps} // Route传递的props,即history location match等
ref={wrappedComponentRef} //ref回调函数
/>
)}
/>
);
};
C.displayName = `withRouter(${Component.displayName || Component.name})`;
C.WrappedComponent = Component;
C.propTypes = {
wrappedComponentRef: PropTypes.func
};
// 将Component组件的静态方法复制到C组件
return hoistStatics(C, Component);
};
export default withRouter;
5. Redirect.js
Redirect组件是react-router中的重定向组件,本身是一个容器组件不做任何实际内容的渲染,其工作流程就是将地址重定向到一个新地址,地址改变后,触发Router组件的回调setState,进而更新整个app。参数如下
- ① push: boolean,
默认false,即重定向的地址会替换当前路径在history历史记录中的位置,如果值为true,即在历史记录中增加重定向的地址,不会删掉当前的地址,和push和repalce的区别一样 - ② from: string, 无默认值, 即页面的来源地址 ③ to: object|string,
无默认值,即将重定向的新地址,可以是object {pathname: '/login', search: '?name=xxx',
state: {type: 1}},对于location当中的信息,当不需要传递参数的时候,可以直接简写to为pathname
源码如下:
import React from "react";
import PropTypes from "prop-types";
import warning from "warning";
import invariant from "invariant";
// createLocation传入path, state, key, currentLocation,返回一个新的location对象
// locationsAreEqual 判断两个location对象的值是否完全相同
import { createLocation, locationsAreEqual } from "history";
import generatePath from "./generatePath"; // 将参数pathname,search 等拼接成一个完成url
class Redirect extends React.Component {
static propTypes = {
computedMatch: PropTypes.object, // Switch组件传递的macth props
push: PropTypes.bool,
from: PropTypes.string,
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
};
static defaultProps = {
push: false
};
// context api
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
push: PropTypes.func.isRequired,
replace: PropTypes.func.isRequired
}).isRequired,
staticContext: PropTypes.object // staticRouter时额外传递的context
}).isRequired
};
// 判断是否是服务端渲染
isStatic() {
return this.context.router && this.context.router.staticContext;
}
componentWillMount() {
invariant(
this.context.router,
"You should not use <Redirect> outside a <Router>"
);
// 服务端渲染时无法使用didMount,在此钩子进行重定向
if (this.isStatic()) this.perform();
}
componentDidMount() {
if (!this.isStatic()) this.perform();
}
componentDidUpdate(prevProps) {
const prevTo = createLocation(prevProps.to); // 上一次重定向的地址
const nextTo = createLocation(this.props.to); // 当前的重定向地址
if (locationsAreEqual(prevTo, nextTo)) {
// 当新旧两个地址完全相同时,控制台打印警告并不进行跳转
warning(
false,
`You tried to redirect to the same route you're currently on: ` +
`"${nextTo.pathname}${nextTo.search}"`
);
return;
}
// 不相同时,进行重定向
this.perform();
}
computeTo({ computedMatch, to }) {
if (computedMatch) {
// 当 当前Redirect组件被外层Switch渲染时,那么将外层Switch传递的params
// 和 Redirect的pathname,组成一个object或者string作为即将要重定向的地址
if (typeof to === "string") {
return generatePath(to, computedMatch.params);
} else {
return {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
};
}
}
return to;
}
perform() {
const { history } = this.context.router; // 获取router api
const { push } = this.props; // 重定向方式
const to = this.computeTo(this.props); // 生成统一的重定向地址string||object
if (push) {
history.push(to);
} else {
history.replace(to);
}
}
// 容器组件不进行任何实际的渲染
render() {
return null;
}
}
export default Redirect;
Redirect作为一个重定向组件,当组件重定向后,组件就会被销毁,那么这个componentDidUpdate在这里存在的意义是什么呢,按照代码层面的理解,它的作用就是提示开发者重定向到了一个重复的地址。思考如下demo
<Switch>
<Redirect from '/album:id' to='/album/5' />
</Switch>
当地址访问'/album/5' 的时候,Redirect的from参数 匹配到了这个路径,然后又将地址重定向到了‘/album/5’,此时又调用顶层Router的render,但是由于地址相同,此时Switch依然会匹配Redirect组件,Redirect组件并没有被销毁,此时就会进行提示,目的就是为了更友好的提示开发者
在此贴一下对这个问题的讨论:链接描述
locationsAreEqual的源码如下:比较简单就不在赘述了,这里依赖了一个第三方库valueEqual,即判断两个object的值是否相等
export const locationsAreEqual = (a, b) =>
a.pathname === b.pathname &&
a.search === b.search &&
a.hash === b.hash &&
a.key === b.key &&
valueEqual(a.state, b.state)
6. generatePath.js
generatePath是react-router组件提供的工具方法,即将传递地址信息path、params处理成一个可访问的pathname
源码如下:
import pathToRegexp from "path-to-regexp";
// 在react-router中只有Redirect使用了此api, 那么我们可以简单将
// patternCache 看作用来缓存进行重定向过的地址信息,此处的优化和在matchPath进行
// 的缓存优化相似
const patternCache = {};
const cacheLimit = 10000;
let cacheCount = 0;
const compileGenerator = pattern => {
const cacheKey = pattern;
// 对于每次将要重定向的地址,首先从本地cache缓存里去查询有无记录,没有记录的
// 的话以重定向地址重新创建一个object
const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
// 如果获取到了记录那么直接返回上次匹配的正则对象
if (cache[pattern]) return cache[pattern];
// 调用pathToRegexp将pathname生成一个函数,此函数可以对对象进行匹配,最终
// 返回一个匹配正确的地址信息,示例demo在下面,也可以访问path-to-regexp的
// 官方地址:https://github.com/pillarjs/path-to-regexp
const compiledGenerator = pathToRegexp.compile(pattern);
// 进行缓存
if (cacheCount < cacheLimit) {
cache[pattern] = compiledGenerator;
cacheCount++;
}
// 返回正则对象的函数
return compiledGenerator;
};
/**
* Public API for generating a URL pathname from a pattern and parameters.
*/
const generatePath = (pattern = "/", params = {}) => {
// 默认重定向地址为根路径,当为根路径时,直接返回
if (pattern === "/") {
return pattern;
}
const generator = compileGenerator(pattern);
// 最终生成一个url地址,这里的pretty: true是path-to-regexp里的一项配置,即只对
// `/?#`地址栏里这三种特殊符合进行转码,其他字符不变。至于为什么这里还需要将Switch
// 匹配到的params传递给将要进行定向的路径不是很理解?即当重定向的路径是 '/user/:id'
// 并且当前地址栏的路径是 '/user/33', 那么重定向地址就会被解析成 '/user/33',即不变
return generator(params, { pretty: true });
};
export default generatePath;
pathToRegexp.compile 示例demo,接收一个pattern参数,最终返回一个url路径,将pattern中的动态路径替换成匹配的对象当中的对应key的value
const toPath = pathToRegexp.compile('/user/:id')
toPath({ id: 123 }) //=> "/user/123"
toPath({ id: 'café' }) //=> "/user/caf%C3%A9"
toPath({ id: '/' }) //=> "/user/%2F"
toPath({ id: ':/' }) //=> "/user/%3A%2F"
toPath({ id: ':/' }, { encode: (value, token) => value }) //=> "/user/:/"
const toPathRepeated = pathToRegexp.compile('/:segment+')
toPathRepeated({ segment: 'foo' }) //=> "/foo"
toPathRepeated({ segment: ['a', 'b', 'c'] }) //=> "/a/b/c"
const toPathRegexp = pathToRegexp.compile('/user/:id(\\d+)')
toPathRegexp({ id: 123 }) //=> "/user/123"
toPathRegexp({ id: '123' }) //=> "/user/123"
toPathRegexp({ id: 'abc' }) //=> Throws `TypeError`.
toPathRegexp({ id: 'abc' }, { noValidate: true }) //=> "/user/abc"
7. Prompt.js
Prompt.js 也许是react-router中很少被用到的组件,它的作用就是可以方便开发者对路由跳转进行 ”拦截“,注意这里并不是真正的拦截,而是react-router自己做到的hack,同时在特殊需求下使用这个组件的时候会引发其他bug,至于原因就不在这里多说了,上一篇文章中花费了很大篇幅来讲这个功能的实现,参数如下
- ① when: boolean, 默认true,即当使用此组件时默认对路由跳转进行拦截处理。
- ② message: string或者func,当为string类型时,即直接展示给用户的提示信息。当为func类型的时候,可以接收(location, action)两个参数,我们可以根据参数和自身的业务选择性的进行拦截,只要不返回string类型 或者 false,router便不会进行拦截处理
源码如下:
import React from "react";
import PropTypes from "prop-types";
import invariant from "invariant";
class Prompt extends React.Component {
static propTypes = {
when: PropTypes.bool,
message: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired
};
static defaultProps = {
when: true // 默认进行拦截
};
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
block: PropTypes.func.isRequired
}).isRequired
}).isRequired
};
enable(message) {
if (this.unblock) this.unblock();
// 讲解除拦截的方法进行返回
this.unblock = this.context.router.history.block(message);
}
disable() {
if (this.unblock) {
this.unblock();
this.unblock = null;
}
}
componentWillMount() {
invariant(
this.context.router,
"You should not use <Prompt> outside a <Router>"
);
if (this.props.when) this.enable(this.props.message);
}
componentWillReceiveProps(nextProps) {
if (nextProps.when) {
// 只有将本次拦截取消后 才能进行修改message的操作
if (!this.props.when || this.props.message !== nextProps.message)
this.enable(nextProps.message);
} else {
// when 改变为false时直接取消
this.disable();
}
}
componentWillUnmount() {
// 销毁后取消拦截
this.disable();
}
render() {
return null;
}
}
export default Prompt;
8 Link.js
Link是react-router中用来进行声明式导航创建的一个组件,与其他组件不同的是,它本身会渲染一个a标签来进行导航,这也是为什么Link.js 和 NavLink.js 会被写在react-router-dom组件库而不是react-router。当然在实际开发中,受限于样式和封装性的影响,直接使用Link或者NavLink的场景并不是很多。先简单介绍一下Link的几个参数
- ① onClick: func, 点击跳转的事件,开发时在跳转前可以在此定义特殊的业务逻辑
- ② target: string, 和a标签的其他属性类似,即 _blank self top 等参数
- ③ replace: boolean, 默认false,即跳转地址的方式,默认使用pushState
- ④ to: string/object, 跳转的地址,可以时字符串即pathname,也可以是一个object包含pathname,search,hash,state等其他参数
- ⑤ innerRef: string/func, a标签的ref,方便获取dom节点
源码如下:
import React from "react";
import PropTypes from "prop-types";
import invariant from "invariant";
import { createLocation } from "history";
// 判断当前的左键点击事件是否使用了复合点击
const isModifiedEvent = event =>
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
class Link extends React.Component {
static propTypes = {
onClick: PropTypes.func,
target: PropTypes.string,
replace: PropTypes.bool,
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
innerRef: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
};
static defaultProps = {
replace: false
};
// 接收Router传递的context api,来进行push 或者 replace操作
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
push: PropTypes.func.isRequired,
replace: PropTypes.func.isRequired,
createHref: PropTypes.func.isRequired
}).isRequired
}).isRequired
};
handleClick = event => {
if (this.props.onClick) this.props.onClick(event); // 跳转前的回调
// 只有以下情况才会使用不刷新的跳转方式来进行导航
// 1.阻止默认事件的方法不存在
// 2.使用的左键进行点击
// 3.不存在target属性
// 4.没有使用复合点击事件进行点击
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!this.props.target && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault(); // 必须要阻止默认事件,否则会走a标签href属性里的地址
const { history } = this.context.router;
const { replace, to } = this.props;
// 进行跳转
if (replace) {
history.replace(to);
} else {
history.push(to);
}
}
};
render() {
const { replace, to, innerRef, ...props } = this.props; // eslint-disable-line no-unused-vars
invariant(
this.context.router,
"You should not use <Link> outside a <Router>"
);
// 必须指定to属性
invariant(to !== undefined, 'You must specify the "to" property');
const { history } = this.context.router;
// 将to转换成一个location对象
const location =
typeof to === "string"
? createLocation(to, null, null, history.location)
: to;
// 将to生成对象的href地址
const href = history.createHref(location);
return (
// 渲染成a标签
<a {...props} onClick={this.handleClick} href={href} ref={innerRef} />
);
}
}
export default Link;
9. NavLink.js
NavLink.js 是Link.js的升级版,主要功能就是对Link添加了激活状态,方便进行导航样式的控制。这里我们可以设想下如何实现这个功能?可以使用Link传递的to参数,生成一个路径然后和当前地址栏的pathname进行匹配,匹配成功的给Link添加activeClass即可。其实NavLink也是这样实现的。参数如下:
- ① to: 即Link当中to,即将跳转的地址,这里还用来进行正则匹配
- ② exact: boolean, 默认false, 即正则匹配到的url是否完全和地址栏pathname相等
- ③ strict: boolean, 默认false, 即最后的 ‘/’ 是否加入匹配
- ④ location: object, 自定义的location匹配对象
- ⑤ activeClassName: string, 即当Link被激活时候的class名称
- ⑥ className: string, 对Link的改写的class名称
- ⑦ activeStyle: object, Link被激活时的样式
- ⑧ style: object, 对Link改写的样式
- ⑨ isAcitve: func, 当Link被匹配到的时候的回调函数,可以再此对匹配到LInk进行自定义的业务逻辑,当返回false时,Link样式也不会被激活
- ⑩ aria-current: string, 当Link被激活时候的html自定义属性
源码如下:
import React from "react";
import PropTypes from "prop-types";
import Route from "./Route";
import Link from "./Link";
const NavLink = ({
to,
exact,
strict,
location,
activeClassName,
className,
activeStyle,
style,
isActive: getIsActive,
"aria-current": ariaCurrent,
...rest
}) => {
const path = typeof to === "object" ? to.pathname : to;
// 看到这里的时候会有一个疑问,为什么要将path里面的特殊符号转义
// 在Switch里一样有对Route Redirect进行劫持的操作,并没有将里面的path进行此操作,
// Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
const escapedPath = path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
return (
<Route
path={escapedPath}
exact={exact}
strict={strict}
location={location}
children={({ location, match }) => {
const isActive = !!(getIsActive ? getIsActive(match, location) : match);
return (
<Link
to={to}
className={
isActive
? [className, activeClassName].filter(i => i).join(" ")
: className
}
style={isActive ? { ...style, ...activeStyle } : style}
aria-current={(isActive && ariaCurrent) || null}
{...rest}
/>
);
}}
/>
);
};
NavLink.propTypes = {
to: Link.propTypes.to,
exact: PropTypes.bool,
strict: PropTypes.bool,
location: PropTypes.object,
activeClassName: PropTypes.string,
className: PropTypes.string,
activeStyle: PropTypes.object,
style: PropTypes.object,
isActive: PropTypes.func,
"aria-current": PropTypes.oneOf([
"page",
"step",
"location",
"date",
"time",
"true"
])
};
NavLink.defaultProps = {
activeClassName: "active",
"aria-current": "page"
};
export default NavLink;
NavLink的to必须要在这里转义的原因什么呢?下面其实列出了原因,即当path当中出现这些特殊字符的时候Link无法被激活,假如NavLink的地址如下:
<NavLink to="/pricewatch/027357/intel-core-i7-7820x-(boxed)">link</NavLink>
点击后页面跳转至 "/pricewatch/027357/intel-core-i7-7820x-(boxed)" 同时 顶层Router 启动新一轮的rerender。
而我们的Route组件一般针对这种动态路由书写的path格式可能是 "/pricewatch/:id/:type" 所以使用这个path生成的正则表达式,对地址栏中的pathname进行匹配是结果的。
但是,在NavLink里,因为to代表的就是实际访问地址,并不是Route当中那个宽泛的path,并且由于to当中包含有 "()" 正则表达式的关键字,在使用path-to-regexp这个库生成的正则表达式就变成了
/^\/pricewatch\/027357\/intel-core-i7-7820x-((?:boxed))(?:\/(?=$))?$/i
其中((?:boxed))变成了子表达式,而地址栏的真实路径却是 "/pricewatch/027357/intel-core-i7-7820x-(boxed)",子表达式部分无法匹配 "(" 这个特殊符号,因此造成matchPath的匹配失败。
所以才需要在NavLink这里对to传递的path进行去正则符号化。
其根本原因是因为Route组件的path设计之初就是为了进行正则匹配,它应该是一个宏观上的宽泛地址。而Link的to参数就是一个实际地址,强行将to设置为path,所以引起了上述bug。下面贴一下官方对这个问题的讨论
链接描述
链接描述
可见,当我们总是追求某些功能组件的复用度时,也许就埋下了未知的bug。当然也无需担心,该来的总会来,有bug了改掉就好
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。