摘要:如果將添加到當前組件,并且當前組件由包裹,那么將引用最外層包裝組件的實例而并非我們期望的當前組件,這也是在實際開發中為什么不推薦使用的原因,使用一個回調函數是一個不錯的選擇,也同樣的使用的是回調函數來實現的。
回顧:上一篇講了BrowserRouter 和 Router之前的關系,以及Router實現路由跳轉切換的原理。這一篇來簡短介紹react-router剩余組件的源碼,結合官方文檔,一起探究實現的的方式。
1. Switch.jsSwitch對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 use2. matchPath.jsoutside a " ); } componentWillReceiveProps(nextProps) { // 這里的兩個警告是說,對于Switch的location這個參數,我們不能做如下兩種操作 // 從無到有和從有到無,猜測這樣做的原因是Switch作為一個渲染控制容器組件,在每次 // 渲染匹配時要做到前后的統一性,即不能第一次使用了地址欄的路徑進行匹配,第二次 // 又使用開發者自定義的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對象,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組件。參數如下:
① :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 use4. withRouter.jsor withRouter() outside a " ); 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 and in the same route; will be ignored" ); // 當同時傳遞了 component 和 children并且children非空,會進行提示 // 并且 children 會被忽略 warning( !( this.props.component && this.props.children && !isEmptyChildren(this.props.children) ), "You should not use and in the same route; will be ignored" ); // 當同時傳遞了 render 和 children并且children非空,會進行提示 // 并且 children 會被忽略 warning( !( this.props.render && this.props.children && !isEmptyChildren(this.props.children) ), "You should not use and in the same route; will be ignored" ); } // 不允許對Route組件的locatin參數 做增刪操作,即Route組件應始終保持初始狀態, // 可以被Router控制,或者被開發者控制,一旦創建則不能進行更改 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." ); // 這里看到并沒有對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 (5. Redirect.js( )} /> ); }; 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。參數如下
① 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 useoutside a " ); // 服務端渲染時無法使用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
當地址訪問"/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 use8 Link.jsoutside a " ); 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的幾個參數
① 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 outside a9. NavLink.js" ); // 必須指定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標簽 ); } } export default Link;
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 ({ 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必須要在這里轉義的原因什么呢?下面其實列出了原因,即當path當中出現這些特殊字符的時候Link無法被激活,假如NavLink的地址如下:
link
點擊后頁面跳轉至 "/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://specialneedsforspecialkids.com/yun/53935.html
摘要:還是先來一段官方的基礎使用案例,熟悉一下整體的代碼流程中使用了端常用到的等一些常用組件,作為的頂層組件來獲取的和設置回調函數來更新。 react-router是react官方推薦并參與維護的一個路由庫,支持瀏覽器端、app端、服務端等常見場景下的路由切換功能,react-router本身不具備切換和跳轉路由的功能,這些功能全部由react-router依賴的history庫完成,his...
摘要:還是先來一段官方的基礎使用案例,熟悉一下整體的代碼流程中使用了端常用到的等一些常用組件,作為的頂層組件來獲取的和設置回調函數來更新。 react-router是react官方推薦并參與維護的一個路由庫,支持瀏覽器端、app端、服務端等常見場景下的路由切換功能,react-router本身不具備切換和跳轉路由的功能,這些功能全部由react-router依賴的history庫完成,his...
摘要:如果將添加到當前組件,并且當前組件由包裹,那么將引用最外層包裝組件的實例而并非我們期望的當前組件,這也是在實際開發中為什么不推薦使用的原因,使用一個回調函數是一個不錯的選擇,也同樣的使用的是回調函數來實現的。 回顧:上一篇講了BrowserRouter 和 Router之前的關系,以及Router實現路由跳轉切換的原理。這一篇來簡短介紹react-router剩余組件的源碼,結合官方文...
閱讀 3229·2021-11-23 10:09
閱讀 2063·2021-10-26 09:51
閱讀 980·2021-10-09 09:44
閱讀 3906·2021-10-08 10:04
閱讀 2745·2021-09-22 15:14
閱讀 3624·2021-09-22 15:02
閱讀 1057·2021-08-24 10:03
閱讀 1727·2019-12-27 12:14