回顾:上一篇讲了BrowserRouter 和 Router以前的关系,以及Router实现路由跳转切换的原理。这一篇来简短介绍react-router剩余组件的源码,结合官方文档,一块儿探究实现的的方式。html
Switch对props.chidlren作遍历筛选,将第一个与pathname匹配到的Route或者Redirect进行渲染(此处只要包含有path这个属性的子节点都会进行筛选,因此能够直接使用自定义的组件,若是缺省path这个属性,而且当匹配到这个子节点时,那么这个子节点就会被渲染同时筛选结束,即Switch里任什么时候刻只渲染惟一一个子节点),当循环结束时仍没有匹配到的子节点返回null。Switch接收两个参数分别是:node
源码以下:
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;
mathPath是react-router用来将path生成正则对象并对pathname进行匹配的一个功能方法。当path不存在时,会直接返回Router的match结果,即当子组件的path不存在时表示该子组件必定会被选渲染(在Switch中若是子节点没有path,并不必定会被渲染,还须要考虑节点被渲染以前不能匹配到其余子节点)。matchPath依赖一个第三方库path-to-regexp,这个库能够将传递的options:path, exact, strict, sensitive 生成一个正则表达式,而后对传递的pathname进行匹配,并返回匹配的结果,服务于Switch,Route组件。参数以下:react
源码以下:
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的官方地址:连接描述git
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 ]
Route.js 是react-router最核心的组件,经过对path进行匹配,来判断是否须要渲染当前组件,它自己也是一个容器组件。细节上须要注意的是,只要path被匹配那么组件就会被渲染,而且Route组件在非Switch包裹的前提下,不受其余组件渲染的影响。当path参数不存在的时候,组件必定会被渲染。github
源码以下:
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;
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;
Redirect组件是react-router中的重定向组件,自己是一个容器组件不作任何实际内容的渲染,其工做流程就是将地址重定向到一个新地址,地址改变后,触发Router组件的回调setState,进而更新整个app。参数以下api
源码以下:
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的值是否相等react-router
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)
generatePath是react-router组件提供的工具方法,即将传递地址信息path、params处理成一个可访问的pathnameapp
源码以下:
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"
Prompt.js 也许是react-router中不多被用到的组件,它的做用就是能够方便开发者对路由跳转进行 ”拦截“,注意这里并非真正的拦截,而是react-router本身作到的hack,同时在特殊需求下使用这个组件的时候会引起其余bug,至于缘由就不在这里多说了,上一篇文章中花费了很大篇幅来说这个功能的实现,参数以下
源码以下:
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;
Link是react-router中用来进行声明式导航建立的一个组件,与其余组件不一样的是,它自己会渲染一个a标签来进行导航,这也是为何Link.js 和 NavLink.js 会被写在react-router-dom组件库而不是react-router。固然在实际开发中,受限于样式和封装性的影响,直接使用Link或者NavLink的场景并非不少。先简单介绍一下Link的几个参数
源码以下:
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;
NavLink.js 是Link.js的升级版,主要功能就是对Link添加了激活状态,方便进行导航样式的控制。这里咱们能够设想下如何实现这个功能?可使用Link传递的to参数,生成一个路径而后和当前地址栏的pathname进行匹配,匹配成功的给Link添加activeClass便可。其实NavLink也是这样实现的。参数以下:
源码以下:
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了改掉就好