回顧:上一篇講了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了改掉就好