摘要:如果將添加到當(dāng)前組件,并且當(dāng)前組件由包裹,那么將引用最外層包裝組件的實(shí)例而并非我們期望的當(dāng)前組件,這也是在實(shí)際開發(fā)中為什么不推薦使用的原因,使用一個(gè)回調(diào)函數(shù)是一個(gè)不錯(cuò)的選擇,也同樣的使用的是回調(diào)函數(shù)來(lái)實(shí)現(xiàn)的。
回顧:上一篇講了BrowserRouter 和 Router之前的關(guān)系,以及Router實(shí)現(xiàn)路由跳轉(zhuǎn)切換的原理。這一篇來(lái)簡(jiǎn)短介紹react-router剩余組件的源碼,結(jié)合官方文檔,一起探究實(shí)現(xiàn)的的方式。
1. Switch.jsSwitch對(duì)props.chidlren做遍歷篩選,將第一個(gè)與pathname匹配到的Route或者Redirect進(jìn)行渲染(此處只要包含有path這個(gè)屬性的子節(jié)點(diǎn)都會(huì)進(jìn)行篩選,所以可以直接使用自定義的組件,如果缺省path這個(gè)屬性,并且當(dāng)匹配到這個(gè)子節(jié)點(diǎn)時(shí),那么這個(gè)子節(jié)點(diǎn)就會(huì)被渲染同時(shí)篩選結(jié)束,即Switch里任何時(shí)刻只渲染唯一一個(gè)子節(jié)點(diǎn)),當(dāng)循環(huán)結(jié)束時(shí)仍沒(méi)有匹配到的子節(jié)點(diǎn)返回null。Switch接收兩個(gè)參數(shù)分別是:
①:location, 開發(fā)者可以填入location參數(shù)來(lái)替換地址欄中的實(shí)際地址進(jìn)行匹配。
②:children,子節(jié)點(diǎn)。
源碼如下:
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內(nèi)部的原因 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 use2. matchPath.jsoutside a " ); } componentWillReceiveProps(nextProps) { // 這里的兩個(gè)警告是說(shuō),對(duì)于Switch的location這個(gè)參數(shù),我們不能做如下兩種操作 // 從無(wú)到有和從有到無(wú),猜測(cè)這樣做的原因是Switch作為一個(gè)渲染控制容器組件,在每次 // 渲染匹配時(shí)要做到前后的統(tǒng)一性,即不能第一次使用了地址欄的路徑進(jìn)行匹配,第二次 // 又使用開發(fā)者自定義的pathname就行匹配 warning( !(nextProps.location && !this.props.location), " 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), " 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對(duì)象,route對(duì)象等。route對(duì)象包含兩個(gè)參數(shù) // 1.location:history.location,即在上一章節(jié)里講到的history這個(gè)庫(kù) // 根據(jù)地址欄的pathname,hash,search,等創(chuàng)建的一個(gè)location對(duì)象。 // 2.match 就是Router組件內(nèi)部的state, 即{path: "/", url: "/", params: {}, isEaxct: true/false} const { route } = this.context.router; const { children } = this.props; // 子節(jié)點(diǎn) // 自定義的location或者Router傳遞的location const location = this.props.location || route.location; // 對(duì)所有子節(jié)點(diǎn)進(jìn)行循環(huán)操作,定義了mactch對(duì)象來(lái)接收匹配到 // 的節(jié)點(diǎn){path,url,parmas,isExact}等信息,當(dāng)子節(jié)點(diǎn)沒(méi)有path這個(gè)屬性的時(shí)候 // 且子節(jié)點(diǎn)被匹配到,那么這個(gè)match會(huì)直接使用Router組件傳遞的match // child就是匹配到子節(jié)點(diǎn) let match, child; React.Children.forEach(children, element => { // 判斷子節(jié)點(diǎn)是否是一個(gè)有效的React節(jié)點(diǎn) // 只有當(dāng)match為null的時(shí)候才會(huì)進(jìn)入匹配的操作,初看的時(shí)候感覺有些奇怪 // 這里主要是matchPath這個(gè)方法做了什么?會(huì)在下一節(jié)講到,這里只需要知道 // matchPath接收了pathname, options={path, exact...},route.match等參數(shù) // 使用正則庫(kù)判斷path是否匹配pathname,如果匹配則會(huì)返回新的macth對(duì)象, // 否則返回null,進(jìn)入下一次的循環(huán)匹配,巧妙如斯 if (match == null && React.isValidElement(element)) { const { path: pathProp, exact, strict, sensitive, from } = element.props; // 從子節(jié)點(diǎn)中獲取props信息,主要是pathProp這個(gè)屬性 // 當(dāng)pathProp不存在時(shí),使用替代的from,否則就是undefined // 這里的from參數(shù)來(lái)自Redirect,即也可以對(duì)redirect進(jìn)行校驗(yàn),來(lái)判斷是否渲染redirect const path = pathProp || from; child = element; match = matchPath( location.pathname, { path, exact, strict, sensitive }, route.match ); } }); // 如果match對(duì)象匹配到了,則調(diào)用cloneElement對(duì)匹配到child子節(jié)點(diǎn)進(jìn)行clone // 操作,并傳遞了兩個(gè)參數(shù)給子節(jié)點(diǎn),location對(duì)象,當(dāng)前的地址信息 // computedMatch對(duì)象,匹配到的路由參數(shù)信息。 return match ? React.cloneElement(child, { location, computedMatch: match }) : null; } } export default Switch;
mathPath是react-router用來(lái)將path生成正則對(duì)象并對(duì)pathname進(jìn)行匹配的一個(gè)功能方法。當(dāng)path不存在時(shí),會(huì)直接返回Router的match結(jié)果,即當(dāng)子組件的path不存在時(shí)表示該子組件一定會(huì)被選渲染(在Switch中如果子節(jié)點(diǎn)沒(méi)有path,并不一定會(huì)被渲染,還需要考慮節(jié)點(diǎn)被渲染之前不能匹配到其他子節(jié)點(diǎn))。matchPath依賴一個(gè)第三方庫(kù)path-to-regexp,這個(gè)庫(kù)可以將傳遞的options:path, exact, strict, sensitive 生成一個(gè)正則表達(dá)式,然后對(duì)傳遞的pathname進(jìn)行匹配,并返回匹配的結(jié)果,服務(wù)于Switch,Route組件。參數(shù)如下:
① :pathname, 真實(shí)的將要被匹配的路徑地址,通常這個(gè)地址是地址欄中的pathname,開發(fā)者也可以自定義傳遞location對(duì)象進(jìn)行替換。
②:options,用來(lái)生成pattern的參數(shù)集合:
path: string, 生成正則當(dāng)中的路徑,比如“/user/:id”,非必填項(xiàng)無(wú)默認(rèn)值
exact: false,默認(rèn)值false。即使用正則匹配到結(jié)果url和pathname是否完全相等,如果傳遞設(shè)置為true,兩者必須完全相等才會(huì)返回macth結(jié)果
strict: false,默認(rèn)值false。即pathname的末尾斜杠會(huì)不會(huì)加入匹配規(guī)則,正常情況下這個(gè)參數(shù)用到的不多。
sensitive: false, 默認(rèn)值false。即正則表達(dá)式是否對(duì)大小寫敏感,同樣用到的不多,不過(guò)某些特殊場(chǎng)景下可能會(huì)用到。
源碼如下:
import pathToRegexp from "path-to-regexp"; // 用來(lái)緩存生成過(guò)的路徑的正則表達(dá)式,如果遇到相同配置規(guī)則且相同路徑的緩存,那么直接使用緩存的正則對(duì)象 const patternCache = {}; const cacheLimit = 10000; // 緩存的最大數(shù)量 let cacheCount = 0; // 已經(jīng)被緩存的個(gè)數(shù) const compilePath = (pattern, options) => { // cacheKey表示配置項(xiàng)的stringify序列化,使用這個(gè)作為patternCache的key const cacheKey = `${options.end}${options.strict}${options.sensitive}`; // 每次先從patternCache中尋找符合當(dāng)前配置項(xiàng)的緩存對(duì)象,如果對(duì)象不存在那么設(shè)置一個(gè) const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {}); // 如果存在以 path 路徑為key的對(duì)象,表示該路徑被生成過(guò),那么直接返回該正則信息 // 至于為什么要做成多層的key來(lái)緩存,即相同的配置項(xiàng)作為第一層key,pattern作為第二層key // 應(yīng)該是即便我們使用obj["xx"]的方式來(lái)調(diào)用某個(gè)值,js內(nèi)部依然是要進(jìn)行遍歷操作的,這樣封裝 // 兩層key,是為了更好的做循環(huán)的優(yōu)化處理,減少了遍歷查找的時(shí)間。 if (cache[pattern]) return cache[pattern]; const keys = []; // 用來(lái)存儲(chǔ)動(dòng)態(tài)路由的參數(shù)key const re = pathToRegexp(pattern, keys, options); const compiledPattern = { re, keys }; //將要被返回的結(jié)果 // 當(dāng)緩存數(shù)量小于10000時(shí),繼續(xù)緩存 if (cacheCount < cacheLimit) { cache[pattern] = compiledPattern; cacheCount++; } // 返回生成的正則表達(dá)式已經(jīng)動(dòng)態(tài)路由的參數(shù) return compiledPattern; }; /** * Public API for matching a URL pathname to a path pattern. */ const matchPath = (pathname, options = {}, parent) => { // options也可以直接傳遞一個(gè)path,其他參數(shù)方法會(huì)自動(dòng)添加默認(rèn)值 if (typeof options === "string") options = { path: options }; // 從options獲取參數(shù),不存在的參數(shù)使用默認(rèn)值 const { path, exact = false, strict = false, sensitive = false } = options; // 當(dāng)path不存在時(shí),直接返回parent,即父級(jí)的match匹配信息 if (path == null) return parent; // 使用options的參數(shù)生成,這里將exact的參數(shù)名改為end,是因?yàn)閜ath-to-regexp用end參數(shù)來(lái)表示 // 是否匹配完整的路徑。即如果默認(rèn)false的情況下,path: /one 和 pathname: /one/two, // path是pathname的一部分,pathname包含了path,那么就會(huì)判斷此次匹配成功 const { re, keys } = compilePath(path, { end: exact, strict, sensitive }); const match = re.exec(pathname); // 對(duì)pathname進(jìn)行匹配 if (!match) return null; // 當(dāng)match不存在時(shí),表示沒(méi)有匹配到,直接返回null // 從match中獲取匹配到的結(jié)果,以一個(gè)path-to-regexp的官方例子來(lái)表示 // 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; // 當(dāng)exact值為true且沒(méi)有完全匹配時(shí)返回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) => { // 獲取動(dòng)態(tài)路由的參數(shù),即傳遞的path: "/:user/:id", pathname: "/xiaohong/23", // params最后返回的結(jié)果就是 {user: xiaohong, id: 23} memo[key.name] = values[index]; return memo; }, {}) }; }; export default matchPath;
簡(jiǎn)單介紹一下path-to-regexp的用法,path-to-regexp的官方地址:鏈接描述
const pathToRegexp = require("path-to-regexp") const keys = [] const regexp = pathToRegexp("/foo/:bar", keys) // regexp = /^/foo/([^/]+?)/?$/i 表示生成的正則表達(dá)式 // keys = [{ name: "bar", prefix: "/", delimiter: "/", optional: false, repeat: false, pattern: "[^/]+?" }] // keys表示動(dòng)態(tài)路由的參數(shù)信息 regexp.exec("/test/route") // 對(duì)pathname進(jìn)行匹配并返回匹配的結(jié)果 //=> [ "/test/route", "test", "route", index: 0, input: "/test/route", groups: undefined ]3. Route.js
Route.js 是react-router最核心的組件,通過(guò)對(duì)path進(jìn)行匹配,來(lái)判斷是否需要渲染當(dāng)前組件,它本身也是一個(gè)容器組件。細(xì)節(jié)上需要注意的是,只要path被匹配那么組件就會(huì)被渲染,并且Route組件在非Switch包裹的前提下,不受其他組件渲染的影響。當(dāng)path參數(shù)不存在的時(shí)候,組件一定會(huì)被渲染。
源碼如下:
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, // 當(dāng)外部使用Switch組件包裹時(shí),此參數(shù)由Switch傳遞進(jìn)來(lái)表示當(dāng)前組件被匹配的信息 path: PropTypes.string, exact: PropTypes.bool, strict: PropTypes.bool, sensitive: PropTypes.bool, component: PropTypes.func, // 組件 render: PropTypes.func, // 一個(gè)渲染函數(shù),函數(shù)的返回結(jié)果為一個(gè)組件或者null,一般用來(lái)做鑒權(quán)操作 children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), // props.children, 子節(jié)點(diǎn) location: PropTypes.object //自定義的location信息 }; // 接收Router組件傳遞的context api static contextTypes = { router: PropTypes.shape({ history: PropTypes.object.isRequired, route: PropTypes.object.isRequired, staticContext: PropTypes.object // 由staticRouter傳遞,服務(wù)端渲染時(shí)會(huì)用到 }) }; // 傳遞給子組件的 context api static childContextTypes = { router: PropTypes.object.isRequired }; // Router組件中也有類似的一套操作,不同的是將Router傳遞的match進(jìn)行了替換,而 // location對(duì)象如果當(dāng)前傳遞了自定義的location,也就會(huì)被替換,否則還是Router組件中傳遞過(guò)來(lái)的location getChildContext() { return { router: { ...this.context.router, route: { location: this.props.location || this.context.router.route.location, match: this.state.match } } }; } // 返回當(dāng)前Route傳遞的options匹配的信息,匹配過(guò)程請(qǐng)看matchPath方法 state = { match: this.computeMatch(this.props, this.context.router) }; computeMatch( { computedMatch, location, path, strict, exact, sensitive }, router ) { // 特殊情況,當(dāng)有computeMatch這個(gè)參數(shù)的時(shí)候,表示當(dāng)前組件是由上層Switch組件 // 已經(jīng)進(jìn)行渲染過(guò)后進(jìn)行clone的組件,那么直接進(jìn)行渲染不需要再進(jìn)行匹配了 if (computedMatch) return computedMatch; invariant( router, "You should not use4. withRouter.jsor withRouter() outside a " ); const { route } = router; //獲取Router組件傳遞的route信息,即包括location、match兩個(gè)對(duì)象 const pathname = (location || route.location).pathname; // 返回matchPath匹配的結(jié)果 return matchPath(pathname, { path, strict, exact, sensitive }, route.match); } componentWillMount() { // 當(dāng)同時(shí)傳遞了component 和 render兩個(gè)props,那么render將會(huì)被忽略 warning( !(this.props.component && this.props.render), "You should not use and in the same route; will be ignored" ); // 當(dāng)同時(shí)傳遞了 component 和 children并且children非空,會(huì)進(jìn)行提示 // 并且 children 會(huì)被忽略 warning( !( this.props.component && this.props.children && !isEmptyChildren(this.props.children) ), "You should not use and in the same route; will be ignored" ); // 當(dāng)同時(shí)傳遞了 render 和 children并且children非空,會(huì)進(jìn)行提示 // 并且 children 會(huì)被忽略 warning( !( this.props.render && this.props.children && !isEmptyChildren(this.props.children) ), "You should not use and in the same route; will be ignored" ); } // 不允許對(duì)Route組件的locatin參數(shù) 做增刪操作,即Route組件應(yīng)始終保持初始狀態(tài), // 可以被Router控制,或者被開發(fā)者控制,一旦創(chuàng)建則不能進(jìn)行更改 componentWillReceiveProps(nextProps, nextContext) { warning( !(nextProps.location && !this.props.location), " 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), " elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render." ); // 這里看到并沒(méi)有對(duì)nextProps和this.props做類似的比較,而是直接進(jìn)行了setState來(lái)進(jìn)行rerender // 結(jié)合上一章節(jié)講述的Router渲染的流程,頂層Router進(jìn)行setState之后,那么所有子Route都需要進(jìn)行 // 重新匹配,然后再渲染對(duì)應(yīng)的節(jié)點(diǎn)數(shù)據(jù) this.setState({ match: this.computeMatch(nextProps, nextContext.router) }); } render() { const { match } = this.state; // matchPath的結(jié)果 const { children, component, render } = this.props; //三種渲染方式 const { history, route, staticContext } = this.context.router; // context router api const location = this.props.location || route.location; // 開發(fā)者自定義的location優(yōu)先級(jí)高 const props = { match, location, history, staticContext }; // 傳遞給子節(jié)點(diǎn)的props數(shù)據(jù) // component優(yōu)先級(jí)最高 if (component) return match ? React.createElement(component, props) : null; // render優(yōu)先級(jí)第二,返回render執(zhí)行后的結(jié)果 if (render) return match ? render(props) : null; // 如果children是一個(gè)函數(shù),那么返回執(zhí)行后的結(jié)果 與render類似 // 此處需要注意即children是不需要進(jìn)行match驗(yàn)證的,即只要Route內(nèi)部 // 嵌套了節(jié)點(diǎn),那么只要不同時(shí)存在component或者render,這個(gè)內(nèi)部節(jié)點(diǎn)一定會(huì)被渲染 if (typeof children === "function") return children(props); // Route內(nèi)的節(jié)點(diǎn)為非空,那么保證當(dāng)前children有一個(gè)包裹的頂層節(jié)點(diǎn)才渲染 if (children && !isEmptyChildren(children)) return React.Children.only(children); // 否則渲染一個(gè)空節(jié)點(diǎn) return null; } } export default Route;
withRouter.js 作為react-router中的唯一HOC,負(fù)責(zé)給非Route組件傳遞context api,即 router: { history, route: {location, match}}。它本身是一個(gè)高階組件,并使用了
hoist-non-react-statics這個(gè)依賴庫(kù),來(lái)保證傳遞的組件的靜態(tài)屬性。
高階組件的另外一個(gè)問(wèn)題就是refs屬性,引用官方文檔的解釋:雖然高階組件的約定是將所有道具傳遞給包裝組件,但這對(duì)于refs不起作用,是因?yàn)閞ef不是真正的prop,它是由react專門處理的。如果將添加到當(dāng)前組件,并且當(dāng)前組件由hoc包裹,那么ref將引用最外層hoc包裝組件的實(shí)例而并非我們期望的當(dāng)前組件,這也是在實(shí)際開發(fā)中為什么不推薦使用refs string的原因,使用一個(gè)回調(diào)函數(shù)是一個(gè)不錯(cuò)的選擇,withRouter也同樣的使用的是回調(diào)函數(shù)來(lái)實(shí)現(xiàn)的。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使用一個(gè)無(wú)狀態(tài)組件 const C = props => { // 接收 wrappedComponentRef屬性來(lái)返回refs,remainingProps保留其他props const { wrappedComponentRef, ...remainingProps } = props; // 實(shí)際返回的是Componetn由Route組件包裝的, 并且沒(méi)有path等屬性保證Component組件一定會(huì)被渲染 return (5. Redirect.js( )} /> ); }; C.displayName = `withRouter(${Component.displayName || Component.name})`; C.WrappedComponent = Component; C.propTypes = { wrappedComponentRef: PropTypes.func }; // 將Component組件的靜態(tài)方法復(fù)制到C組件 return hoistStatics(C, Component); }; export default withRouter;
Redirect組件是react-router中的重定向組件,本身是一個(gè)容器組件不做任何實(shí)際內(nèi)容的渲染,其工作流程就是將地址重定向到一個(gè)新地址,地址改變后,觸發(fā)Router組件的回調(diào)setState,進(jìn)而更新整個(gè)app。參數(shù)如下
① push: boolean,
默認(rèn)false,即重定向的地址會(huì)替換當(dāng)前路徑在history歷史記錄中的位置,如果值為true,即在歷史記錄中增加重定向的地址,不會(huì)刪掉當(dāng)前的地址,和push和repalce的區(qū)別一樣
② from: string, 無(wú)默認(rèn)值, 即頁(yè)面的來(lái)源地址 ③ to: object|string,
無(wú)默認(rèn)值,即將重定向的新地址,可以是object {pathname: "/login", search: "?name=xxx",
state: {type: 1}},對(duì)于location當(dāng)中的信息,當(dāng)不需要傳遞參數(shù)的時(shí)候,可以直接簡(jiǎn)寫to為pathname
源碼如下:
import React from "react"; import PropTypes from "prop-types"; import warning from "warning"; import invariant from "invariant"; // createLocation傳入path, state, key, currentLocation,返回一個(gè)新的location對(duì)象 // locationsAreEqual 判斷兩個(gè)location對(duì)象的值是否完全相同 import { createLocation, locationsAreEqual } from "history"; import generatePath from "./generatePath"; // 將參數(shù)pathname,search 等拼接成一個(gè)完成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時(shí)額外傳遞的context }).isRequired }; // 判斷是否是服務(wù)端渲染 isStatic() { return this.context.router && this.context.router.staticContext; } componentWillMount() { invariant( this.context.router, "You should not useoutside a " ); // 服務(wù)端渲染時(shí)無(wú)法使用didMount,在此鉤子進(jìn)行重定向 if (this.isStatic()) this.perform(); } componentDidMount() { if (!this.isStatic()) this.perform(); } componentDidUpdate(prevProps) { const prevTo = createLocation(prevProps.to); // 上一次重定向的地址 const nextTo = createLocation(this.props.to); // 當(dāng)前的重定向地址 if (locationsAreEqual(prevTo, nextTo)) { // 當(dāng)新舊兩個(gè)地址完全相同時(shí),控制臺(tái)打印警告并不進(jìn)行跳轉(zhuǎn) warning( false, `You tried to redirect to the same route you"re currently on: ` + `"${nextTo.pathname}${nextTo.search}"` ); return; } // 不相同時(shí),進(jìn)行重定向 this.perform(); } computeTo({ computedMatch, to }) { if (computedMatch) { // 當(dāng) 當(dāng)前Redirect組件被外層Switch渲染時(shí),那么將外層Switch傳遞的params // 和 Redirect的pathname,組成一個(gè)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); // 生成統(tǒng)一的重定向地址string||object if (push) { history.push(to); } else { history.replace(to); } } // 容器組件不進(jìn)行任何實(shí)際的渲染 render() { return null; } } export default Redirect;
Redirect作為一個(gè)重定向組件,當(dāng)組件重定向后,組件就會(huì)被銷毀,那么這個(gè)componentDidUpdate在這里存在的意義是什么呢,按照代碼層面的理解,它的作用就是提示開發(fā)者重定向到了一個(gè)重復(fù)的地址。思考如下demo
當(dāng)?shù)刂吩L問(wèn)"/album/5" 的時(shí)候,Redirect的from參數(shù) 匹配到了這個(gè)路徑,然后又將地址重定向到了‘/album/5’,此時(shí)又調(diào)用頂層Router的render,但是由于地址相同,此時(shí)Switch依然會(huì)匹配Redirect組件,Redirect組件并沒(méi)有被銷毀,此時(shí)就會(huì)進(jìn)行提示,目的就是為了更友好的提示開發(fā)者
在此貼一下對(duì)這個(gè)問(wèn)題的討論:鏈接描述
locationsAreEqual的源碼如下:比較簡(jiǎn)單就不在贅述了,這里依賴了一個(gè)第三方庫(kù)valueEqual,即判斷兩個(gè)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處理成一個(gè)可訪問(wèn)的pathname
源碼如下:
import pathToRegexp from "path-to-regexp"; // 在react-router中只有Redirect使用了此api, 那么我們可以簡(jiǎn)單將 // patternCache 看作用來(lái)緩存進(jìn)行重定向過(guò)的地址信息,此處的優(yōu)化和在matchPath進(jìn)行 // 的緩存優(yōu)化相似 const patternCache = {}; const cacheLimit = 10000; let cacheCount = 0; const compileGenerator = pattern => { const cacheKey = pattern; // 對(duì)于每次將要重定向的地址,首先從本地cache緩存里去查詢有無(wú)記錄,沒(méi)有記錄的 // 的話以重定向地址重新創(chuàng)建一個(gè)object const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {}); // 如果獲取到了記錄那么直接返回上次匹配的正則對(duì)象 if (cache[pattern]) return cache[pattern]; // 調(diào)用pathToRegexp將pathname生成一個(gè)函數(shù),此函數(shù)可以對(duì)對(duì)象進(jìn)行匹配,最終 // 返回一個(gè)匹配正確的地址信息,示例demo在下面,也可以訪問(wèn)path-to-regexp的 // 官方地址:https://github.com/pillarjs/path-to-regexp const compiledGenerator = pathToRegexp.compile(pattern); // 進(jìn)行緩存 if (cacheCount < cacheLimit) { cache[pattern] = compiledGenerator; cacheCount++; } // 返回正則對(duì)象的函數(shù) return compiledGenerator; }; /** * Public API for generating a URL pathname from a pattern and parameters. */ const generatePath = (pattern = "/", params = {}) => { // 默認(rèn)重定向地址為根路徑,當(dāng)為根路徑時(shí),直接返回 if (pattern === "/") { return pattern; } const generator = compileGenerator(pattern); // 最終生成一個(gè)url地址,這里的pretty: true是path-to-regexp里的一項(xiàng)配置,即只對(duì) // `/?#`地址欄里這三種特殊符合進(jìn)行轉(zhuǎn)碼,其他字符不變。至于為什么這里還需要將Switch // 匹配到的params傳遞給將要進(jìn)行定向的路徑不是很理解?即當(dāng)重定向的路徑是 "/user/:id" // 并且當(dāng)前地址欄的路徑是 "/user/33", 那么重定向地址就會(huì)被解析成 "/user/33",即不變 return generator(params, { pretty: true }); }; export default generatePath;
pathToRegexp.compile 示例demo,接收一個(gè)pattern參數(shù),最終返回一個(gè)url路徑,將pattern中的動(dòng)態(tài)路徑替換成匹配的對(duì)象當(dāng)中的對(duì)應(yīng)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中很少被用到的組件,它的作用就是可以方便開發(fā)者對(duì)路由跳轉(zhuǎn)進(jìn)行 ”攔截“,注意這里并不是真正的攔截,而是react-router自己做到的hack,同時(shí)在特殊需求下使用這個(gè)組件的時(shí)候會(huì)引發(fā)其他bug,至于原因就不在這里多說(shuō)了,上一篇文章中花費(fèi)了很大篇幅來(lái)講這個(gè)功能的實(shí)現(xiàn),參數(shù)如下
① when: boolean, 默認(rèn)true,即當(dāng)使用此組件時(shí)默認(rèn)對(duì)路由跳轉(zhuǎn)進(jìn)行攔截處理。
② message: string或者func,當(dāng)為string類型時(shí),即直接展示給用戶的提示信息。當(dāng)為func類型的時(shí)候,可以接收(location, action)兩個(gè)參數(shù),我們可以根據(jù)參數(shù)和自身的業(yè)務(wù)選擇性的進(jìn)行攔截,只要不返回string類型 或者 false,router便不會(huì)進(jìn)行攔截處理
源碼如下:
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 // 默認(rèn)進(jìn)行攔截 }; static contextTypes = { router: PropTypes.shape({ history: PropTypes.shape({ block: PropTypes.func.isRequired }).isRequired }).isRequired }; enable(message) { if (this.unblock) this.unblock(); // 講解除攔截的方法進(jìn)行返回 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 use8 Link.jsoutside a " ); if (this.props.when) this.enable(this.props.message); } componentWillReceiveProps(nextProps) { if (nextProps.when) { // 只有將本次攔截取消后 才能進(jìn)行修改message的操作 if (!this.props.when || this.props.message !== nextProps.message) this.enable(nextProps.message); } else { // when 改變?yōu)閒alse時(shí)直接取消 this.disable(); } } componentWillUnmount() { // 銷毀后取消攔截 this.disable(); } render() { return null; } } export default Prompt;
Link是react-router中用來(lái)進(jìn)行聲明式導(dǎo)航創(chuàng)建的一個(gè)組件,與其他組件不同的是,它本身會(huì)渲染一個(gè)a標(biāo)簽來(lái)進(jìn)行導(dǎo)航,這也是為什么Link.js 和 NavLink.js 會(huì)被寫在react-router-dom組件庫(kù)而不是react-router。當(dāng)然在實(shí)際開發(fā)中,受限于樣式和封裝性的影響,直接使用Link或者NavLink的場(chǎng)景并不是很多。先簡(jiǎn)單介紹一下Link的幾個(gè)參數(shù)
① onClick: func, 點(diǎn)擊跳轉(zhuǎn)的事件,開發(fā)時(shí)在跳轉(zhuǎn)前可以在此定義特殊的業(yè)務(wù)邏輯
② target: string, 和a標(biāo)簽的其他屬性類似,即 _blank self top 等參數(shù)
③ replace: boolean, 默認(rèn)false,即跳轉(zhuǎn)地址的方式,默認(rèn)使用pushState
④ to: string/object, 跳轉(zhuǎn)的地址,可以時(shí)字符串即pathname,也可以是一個(gè)object包含pathname,search,hash,state等其他參數(shù)
⑤ innerRef: string/func, a標(biāo)簽的ref,方便獲取dom節(jié)點(diǎn)
源碼如下:
import React from "react"; import PropTypes from "prop-types"; import invariant from "invariant"; import { createLocation } from "history"; // 判斷當(dāng)前的左鍵點(diǎn)擊事件是否使用了復(fù)合點(diǎn)擊 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,來(lái)進(jìn)行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); // 跳轉(zhuǎn)前的回調(diào) // 只有以下情況才會(huì)使用不刷新的跳轉(zhuǎn)方式來(lái)進(jìn)行導(dǎo)航 // 1.阻止默認(rèn)事件的方法不存在 // 2.使用的左鍵進(jìn)行點(diǎn)擊 // 3.不存在target屬性 // 4.沒(méi)有使用復(fù)合點(diǎn)擊事件進(jìn)行點(diǎn)擊 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(); // 必須要阻止默認(rèn)事件,否則會(huì)走a標(biāo)簽href屬性里的地址 const { history } = this.context.router; const { replace, to } = this.props; // 進(jìn)行跳轉(zhuǎn) 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 outside a9. NavLink.js" ); // 必須指定to屬性 invariant(to !== undefined, "You must specify the "to" property"); const { history } = this.context.router; // 將to轉(zhuǎn)換成一個(gè)location對(duì)象 const location = typeof to === "string" ? createLocation(to, null, null, history.location) : to; // 將to生成對(duì)象的href地址 const href = history.createHref(location); return ( // 渲染成a標(biāo)簽 ); } } export default Link;
NavLink.js 是Link.js的升級(jí)版,主要功能就是對(duì)Link添加了激活狀態(tài),方便進(jìn)行導(dǎo)航樣式的控制。這里我們可以設(shè)想下如何實(shí)現(xiàn)這個(gè)功能?可以使用Link傳遞的to參數(shù),生成一個(gè)路徑然后和當(dāng)前地址欄的pathname進(jìn)行匹配,匹配成功的給Link添加activeClass即可。其實(shí)NavLink也是這樣實(shí)現(xiàn)的。參數(shù)如下:
① to: 即Link當(dāng)中to,即將跳轉(zhuǎn)的地址,這里還用來(lái)進(jìn)行正則匹配
② exact: boolean, 默認(rèn)false, 即正則匹配到的url是否完全和地址欄pathname相等
③ strict: boolean, 默認(rèn)false, 即最后的 ‘/’ 是否加入匹配
④ location: object, 自定義的location匹配對(duì)象
⑤ activeClassName: string, 即當(dāng)Link被激活時(shí)候的class名稱
⑥ className: string, 對(duì)Link的改寫的class名稱
⑦ activeStyle: object, Link被激活時(shí)的樣式
⑧ style: object, 對(duì)Link改寫的樣式
⑨ isAcitve: func, 當(dāng)Link被匹配到的時(shí)候的回調(diào)函數(shù),可以再此對(duì)匹配到LInk進(jìn)行自定義的業(yè)務(wù)邏輯,當(dāng)返回false時(shí),Link樣式也不會(huì)被激活
⑩ aria-current: string, 當(dāng)Link被激活時(shí)候的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; // 看到這里的時(shí)候會(huì)有一個(gè)疑問(wèn),為什么要將path里面的特殊符號(hào)轉(zhuǎn)義 // 在Switch里一樣有對(duì)Route Redirect進(jìn)行劫持的操作,并沒(méi)有將里面的path進(jìn)行此操作, // Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202 const escapedPath = path && path.replace(/([.+*?=^!:${}()[]|/])/g, "$1"); return ({ const isActive = !!(getIsActive ? getIsActive(match, location) : match); return ( 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必須要在這里轉(zhuǎn)義的原因什么呢?下面其實(shí)列出了原因,即當(dāng)path當(dāng)中出現(xiàn)這些特殊字符的時(shí)候Link無(wú)法被激活,假如NavLink的地址如下:
link
點(diǎn)擊后頁(yè)面跳轉(zhuǎn)至 "/pricewatch/027357/intel-core-i7-7820x-(boxed)" 同時(shí) 頂層Router 啟動(dòng)新一輪的rerender。
而我們的Route組件一般針對(duì)這種動(dòng)態(tài)路由書寫的path格式可能是 "/pricewatch/:id/:type" 所以使用這個(gè)path生成的正則表達(dá)式,對(duì)地址欄中的pathname進(jìn)行匹配是結(jié)果的。
但是,在NavLink里,因?yàn)閠o代表的就是實(shí)際訪問(wèn)地址,并不是Route當(dāng)中那個(gè)寬泛的path,并且由于to當(dāng)中包含有 "()" 正則表達(dá)式的關(guān)鍵字,在使用path-to-regexp這個(gè)庫(kù)生成的正則表達(dá)式就變成了
/^/pricewatch/027357/intel-core-i7-7820x-((?:boxed))(?:/(?=$))?$/i
其中((?:boxed))變成了子表達(dá)式,而地址欄的真實(shí)路徑卻是 "/pricewatch/027357/intel-core-i7-7820x-(boxed)",子表達(dá)式部分無(wú)法匹配 "(" 這個(gè)特殊符號(hào),因此造成matchPath的匹配失敗。
所以才需要在NavLink這里對(duì)to傳遞的path進(jìn)行去正則符號(hào)化。
其根本原因是因?yàn)镽oute組件的path設(shè)計(jì)之初就是為了進(jìn)行正則匹配,它應(yīng)該是一個(gè)宏觀上的寬泛地址。而Link的to參數(shù)就是一個(gè)實(shí)際地址,強(qiáng)行將to設(shè)置為path,所以引起了上述bug。下面貼一下官方對(duì)這個(gè)問(wèn)題的討論
鏈接描述
鏈接描述
可見,當(dāng)我們總是追求某些功能組件的復(fù)用度時(shí),也許就埋下了未知的bug。當(dāng)然也無(wú)需擔(dān)心,該來(lái)的總會(huì)來(lái),有bug了改掉就好
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/109813.html
摘要:還是先來(lái)一段官方的基礎(chǔ)使用案例,熟悉一下整體的代碼流程中使用了端常用到的等一些常用組件,作為的頂層組件來(lái)獲取的和設(shè)置回調(diào)函數(shù)來(lái)更新。 react-router是react官方推薦并參與維護(hù)的一個(gè)路由庫(kù),支持瀏覽器端、app端、服務(wù)端等常見場(chǎng)景下的路由切換功能,react-router本身不具備切換和跳轉(zhuǎn)路由的功能,這些功能全部由react-router依賴的history庫(kù)完成,his...
摘要:還是先來(lái)一段官方的基礎(chǔ)使用案例,熟悉一下整體的代碼流程中使用了端常用到的等一些常用組件,作為的頂層組件來(lái)獲取的和設(shè)置回調(diào)函數(shù)來(lái)更新。 react-router是react官方推薦并參與維護(hù)的一個(gè)路由庫(kù),支持瀏覽器端、app端、服務(wù)端等常見場(chǎng)景下的路由切換功能,react-router本身不具備切換和跳轉(zhuǎn)路由的功能,這些功能全部由react-router依賴的history庫(kù)完成,his...
摘要:如果將添加到當(dāng)前組件,并且當(dāng)前組件由包裹,那么將引用最外層包裝組件的實(shí)例而并非我們期望的當(dāng)前組件,這也是在實(shí)際開發(fā)中為什么不推薦使用的原因,使用一個(gè)回調(diào)函數(shù)是一個(gè)不錯(cuò)的選擇,也同樣的使用的是回調(diào)函數(shù)來(lái)實(shí)現(xiàn)的。 回顧:上一篇講了BrowserRouter 和 Router之前的關(guān)系,以及Router實(shí)現(xiàn)路由跳轉(zhuǎn)切換的原理。這一篇來(lái)簡(jiǎn)短介紹react-router剩余組件的源碼,結(jié)合官方文...
閱讀 2573·2021-10-08 10:04
閱讀 2735·2021-09-06 15:02
閱讀 791·2019-08-30 13:50
閱讀 1547·2019-08-30 13:21
閱讀 2586·2019-08-30 11:15
閱讀 2113·2019-08-29 17:19
閱讀 1574·2019-08-26 13:55
閱讀 1260·2019-08-26 10:15