国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

react-router v4.x 源碼拾遺2

CoorChice / 1107人閱讀

摘要:如果將添加到當前組件,并且當前組件由包裹,那么將引用最外層包裝組件的實例而并非我們期望的當前組件,這也是在實際開發中為什么不推薦使用的原因,使用一個回調函數是一個不錯的選擇,也同樣的使用的是回調函數來實現的。

回顧:上一篇講了BrowserRouter 和 Router之前的關系,以及Router實現路由跳轉切換的原理。這一篇來簡短介紹react-router剩余組件的源碼,結合官方文檔,一起探究實現的的方式。

1. Switch.js

Switch對props.chidlren做遍歷篩選,將第一個與pathname匹配到的Route或者Redirect進行渲染(此處只要包含有path這個屬性的子節點都會進行篩選,所以可以直接使用自定義的組件,如果缺省path這個屬性,并且當匹配到這個子節點時,那么這個子節點就會被渲染同時篩選結束,即Switch里任何時刻只渲染唯一一個子節點),當循環結束時仍沒有匹配到的子節點返回null。Switch接收兩個參數分別是:

①:location, 開發者可以填入location參數來替換地址欄中的實際地址進行匹配。

②:children,子節點。

源碼如下:
import React from "react";
import PropTypes from "prop-types";
import warning from "warning";
import invariant from "invariant";
import matchPath from "./matchPath";

class Switch extends React.Component {
    // 接收Router組件傳遞的context api,這也是為什么Switch要寫在
    // Router內部的原因    
  static contextTypes = {
    router: PropTypes.shape({
      route: PropTypes.object.isRequired
    }).isRequired
  };

  static propTypes = {
    children: PropTypes.node,
    location: PropTypes.object
  };

  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use  outside 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;
2. matchPath.js

mathPath是react-router用來將path生成正則對象并對pathname進行匹配的一個功能方法。當path不存在時,會直接返回Router的match結果,即當子組件的path不存在時表示該子組件一定會被選渲染(在Switch中如果子節點沒有path,并不一定會被渲染,還需要考慮節點被渲染之前不能匹配到其他子節點)。matchPath依賴一個第三方庫path-to-regexp,這個庫可以將傳遞的options:path, exact, strict, sensitive 生成一個正則表達式,然后對傳遞的pathname進行匹配,并返回匹配的結果,服務于Switch,Route組件。參數如下:

① :pathname, 真實的將要被匹配的路徑地址,通常這個地址是地址欄中的pathname,開發者也可以自定義傳遞location對象進行替換。

②:options,用來生成pattern的參數集合:
path: string, 生成正則當中的路徑,比如“/user/:id”,非必填項無默認值
exact: false,默認值false。即使用正則匹配到結果url和pathname是否完全相等,如果傳遞設置為true,兩者必須完全相等才會返回macth結果
strict: false,默認值false。即pathname的末尾斜杠會不會加入匹配規則,正常情況下這個參數用到的不多。
sensitive: false, 默認值false。即正則表達式是否對大小寫敏感,同樣用到的不多,不過某些特殊場景下可能會用到。

源碼如下:
import pathToRegexp from "path-to-regexp";
// 用來緩存生成過的路徑的正則表達式,如果遇到相同配置規則且相同路徑的緩存,那么直接使用緩存的正則對象
const patternCache = {}; 
const cacheLimit = 10000; // 緩存的最大數量
let cacheCount = 0; // 已經被緩存的個數

const compilePath = (pattern, options) => {
    // cacheKey表示配置項的stringify序列化,使用這個作為patternCache的key
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  // 每次先從patternCache中尋找符合當前配置項的緩存對象,如果對象不存在那么設置一個
  const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
   // 如果存在以 path 路徑為key的對象,表示該路徑被生成過,那么直接返回該正則信息
   // 至于為什么要做成多層的key來緩存,即相同的配置項作為第一層key,pattern作為第二層key
   // 應該是即便我們使用obj["xx"]的方式來調用某個值,js內部依然是要進行遍歷操作的,這樣封裝
   // 兩層key,是為了更好的做循環的優化處理,減少了遍歷查找的時間。
  if (cache[pattern]) return cache[pattern];

  const keys = []; // 用來存儲動態路由的參數key
  const re = pathToRegexp(pattern, keys, options);
  const compiledPattern = { re, keys }; //將要被返回的結果
    // 當緩存數量小于10000時,繼續緩存
  if (cacheCount < cacheLimit) {
    cache[pattern] = compiledPattern;
    cacheCount++;
  }
    // 返回生成的正則表達式已經動態路由的參數
  return compiledPattern;
};

/**
 * Public API for matching a URL pathname to a path pattern.
 */
const matchPath = (pathname, options = {}, parent) => {
    // options也可以直接傳遞一個path,其他參數方法會自動添加默認值
  if (typeof options === "string") options = { path: options };
    // 從options獲取參數,不存在的參數使用默認值
  const { path, exact = false, strict = false, sensitive = false } = options;
    // 當path不存在時,直接返回parent,即父級的match匹配信息
  if (path == null) return parent;
    // 使用options的參數生成,這里將exact的參數名改為end,是因為path-to-regexp用end參數來表示
    // 是否匹配完整的路徑。即如果默認false的情況下,path: /one 和 pathname: /one/two,
    // path是pathname的一部分,pathname包含了path,那么就會判斷此次匹配成功
  const { re, keys } = compilePath(path, { end: exact, strict, sensitive });
  const match = re.exec(pathname); // 對pathname進行匹配

  if (!match) return null; // 當match不存在時,表示沒有匹配到,直接返回null
     // 從match中獲取匹配到的結果,以一個path-to-regexp的官方例子來表示
     // const keys = []
     // const regexp = pathToRegexp("/:foo/:bar", keys)
    // regexp.exec("/test/route")
    //=> [ "/test/route", "test", "route", index: 0, input: "/test/route", groups: undefined ]
  const [url, ...values] = match;
  const isExact = pathname === url; // 判斷是否完全匹配

  if (exact && !isExact) return null; // 當exact值為true且沒有完全匹配時返回null

  return {
    path, // the path pattern used to match
    url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
    isExact, // whether or not we matched exactly
    params: keys.reduce((memo, key, index) => {
        // 獲取動態路由的參數,即傳遞的path: "/:user/:id", pathname: "/xiaohong/23",
        // params最后返回的結果就是 {user: xiaohong, id: 23}
      memo[key.name] = values[index];
      return memo;
    }, {})
  };
};

export default matchPath;

簡單介紹一下path-to-regexp的用法,path-to-regexp的官方地址:鏈接描述

const pathToRegexp = require("path-to-regexp")
const keys = []
const regexp = pathToRegexp("/foo/:bar", keys)
// regexp = /^/foo/([^/]+?)/?$/i  表示生成的正則表達式
// keys = [{ name: "bar", prefix: "/", delimiter: "/", optional: false, repeat: false, pattern: "[^/]+?" }]
// keys表示動態路由的參數信息
regexp.exec("/test/route") // 對pathname進行匹配并返回匹配的結果
//=> [ "/test/route", "test", "route", index: 0, input: "/test/route", groups: undefined ]
3. Route.js

Route.js 是react-router最核心的組件,通過對path進行匹配,來判斷是否需要渲染當前組件,它本身也是一個容器組件。細節上需要注意的是,只要path被匹配那么組件就會被渲染,并且Route組件在非Switch包裹的前提下,不受其他組件渲染的影響。當path參數不存在的時候,組件一定會被渲染。

源碼如下:
import warning from "warning";
import invariant from "invariant";
import React from "react";
import PropTypes from "prop-types";
import matchPath from "./matchPath";
// 判斷children是否為空
const isEmptyChildren = children => React.Children.count(children) === 0;
class Route extends React.Component {
  static propTypes = {
    computedMatch: PropTypes.object, // 當外部使用Switch組件包裹時,此參數由Switch傳遞進來表示當前組件被匹配的信息
    path: PropTypes.string,
    exact: PropTypes.bool,
    strict: PropTypes.bool,
    sensitive: PropTypes.bool,
    component: PropTypes.func, // 組件
    render: PropTypes.func, // 一個渲染函數,函數的返回結果為一個組件或者null,一般用來做鑒權操作
    children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), // props.children, 子節點
    location: PropTypes.object //自定義的location信息
  };
    // 接收Router組件傳遞的context api
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.object.isRequired,
      route: PropTypes.object.isRequired,
      staticContext: PropTypes.object // 由staticRouter傳遞,服務端渲染時會用到
    })
  };
    // 傳遞給子組件的 context api
  static childContextTypes = {
    router: PropTypes.object.isRequired
  };
    // Router組件中也有類似的一套操作,不同的是將Router傳遞的match進行了替換,而
    // location對象如果當前傳遞了自定義的location,也就會被替換,否則還是Router組件中傳遞過來的location
  getChildContext() {
    return {
      router: {
        ...this.context.router,
        route: {
          location: this.props.location || this.context.router.route.location,
          match: this.state.match
        }
      }
    };
  }
    // 返回當前Route傳遞的options匹配的信息,匹配過程請看matchPath方法
  state = {
    match: this.computeMatch(this.props, this.context.router)
  };

  computeMatch(
    { computedMatch, location, path, strict, exact, sensitive },
    router
  ) {
      // 特殊情況,當有computeMatch這個參數的時候,表示當前組件是由上層Switch組件
      // 已經進行渲染過后進行clone的組件,那么直接進行渲染不需要再進行匹配了
    if (computedMatch) return computedMatch;

    invariant(
      router,
      "You should not use  or 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;
4. withRouter.js

withRouter.js 作為react-router中的唯一HOC,負責給非Route組件傳遞context api,即 router: { history, route: {location, match}}。它本身是一個高階組件,并使用了
hoist-non-react-statics這個依賴庫,來保證傳遞的組件的靜態屬性。
高階組件的另外一個問題就是refs屬性,引用官方文檔的解釋:雖然高階組件的約定是將所有道具傳遞給包裝組件,但這對于refs不起作用,是因為ref不是真正的prop,它是由react專門處理的。如果將添加到當前組件,并且當前組件由hoc包裹,那么ref將引用最外層hoc包裝組件的實例而并非我們期望的當前組件,這也是在實際開發中為什么不推薦使用refs string的原因,使用一個回調函數是一個不錯的選擇,withRouter也同樣的使用的是回調函數來實現的。react官方推薦的解決方案是 React.forwardRef API(16.3版本), 地址如下:鏈接描述

源碼如下:
import React from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";
import Route from "./Route"; 
// withRouter使用的也是Route容器組件,這樣Component就可以直接使用props獲取到history等api

const withRouter = Component => {
    // withRouter使用一個無狀態組件
  const C = props => {
      // 接收 wrappedComponentRef屬性來返回refs,remainingProps保留其他props
    const { wrappedComponentRef, ...remainingProps } = props;
    // 實際返回的是Componetn由Route組件包裝的, 并且沒有path等屬性保證Component組件一定會被渲染
    return (
       (
          
        )}
      />
    );
  };

  C.displayName = `withRouter(${Component.displayName || Component.name})`;
  C.WrappedComponent = Component;
  C.propTypes = {
    wrappedComponentRef: PropTypes.func
  };
    // 將Component組件的靜態方法復制到C組件
  return hoistStatics(C, Component);
};

export default withRouter;
5. Redirect.js

Redirect組件是react-router中的重定向組件,本身是一個容器組件不做任何實際內容的渲染,其工作流程就是將地址重定向到一個新地址,地址改變后,觸發Router組件的回調setState,進而更新整個app。參數如下

① push: boolean,
默認false,即重定向的地址會替換當前路徑在history歷史記錄中的位置,如果值為true,即在歷史記錄中增加重定向的地址,不會刪掉當前的地址,和push和repalce的區別一樣

② from: string, 無默認值, 即頁面的來源地址 ③ to: object|string,
無默認值,即將重定向的新地址,可以是object {pathname: "/login", search: "?name=xxx",
state: {type: 1}},對于location當中的信息,當不需要傳遞參數的時候,可以直接簡寫to為pathname

源碼如下:
import React from "react";
import PropTypes from "prop-types";
import warning from "warning";
import invariant from "invariant";
// createLocation傳入path, state, key, currentLocation,返回一個新的location對象
// locationsAreEqual 判斷兩個location對象的值是否完全相同
import { createLocation, locationsAreEqual } from "history"; 
import generatePath from "./generatePath"; // 將參數pathname,search 等拼接成一個完成url

class Redirect extends React.Component {
  static propTypes = {
    computedMatch: PropTypes.object, // Switch組件傳遞的macth props
    push: PropTypes.bool,
    from: PropTypes.string,
    to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
  };

  static defaultProps = {
    push: false
  };
    // context api
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired
      }).isRequired,
      staticContext: PropTypes.object // staticRouter時額外傳遞的context
    }).isRequired
  };
    // 判斷是否是服務端渲染
  isStatic() {
    return this.context.router && this.context.router.staticContext;
  }

  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use  outside 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 use  outside 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;
8 Link.js

Link是react-router中用來進行聲明式導航創建的一個組件,與其他組件不同的是,它本身會渲染一個a標簽來進行導航,這也是為什么Link.js 和 NavLink.js 會被寫在react-router-dom組件庫而不是react-router。當然在實際開發中,受限于樣式和封裝性的影響,直接使用Link或者NavLink的場景并不是很多。先簡單介紹一下Link的幾個參數

① onClick: func, 點擊跳轉的事件,開發時在跳轉前可以在此定義特殊的業務邏輯

② target: string, 和a標簽的其他屬性類似,即 _blank self top 等參數

③ replace: boolean, 默認false,即跳轉地址的方式,默認使用pushState

④ to: string/object, 跳轉的地址,可以時字符串即pathname,也可以是一個object包含pathname,search,hash,state等其他參數

⑤ innerRef: string/func, a標簽的ref,方便獲取dom節點

源碼如下:
import React from "react";
import PropTypes from "prop-types";
import invariant from "invariant";
import { createLocation } from "history";

// 判斷當前的左鍵點擊事件是否使用了復合點擊
const isModifiedEvent = event =>
  !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);

class Link extends React.Component {
  static propTypes = {
    onClick: PropTypes.func,
    target: PropTypes.string,
    replace: PropTypes.bool,
    to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
    innerRef: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
  };

  static defaultProps = {
    replace: false
  };
    // 接收Router傳遞的context api,來進行push 或者 replace操作
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired,
        createHref: PropTypes.func.isRequired
      }).isRequired
    }).isRequired
  };

  handleClick = event => {
    if (this.props.onClick) this.props.onClick(event); // 跳轉前的回調
    // 只有以下情況才會使用不刷新的跳轉方式來進行導航
    // 1.阻止默認事件的方法不存在
    // 2.使用的左鍵進行點擊
    // 3.不存在target屬性
    // 4.沒有使用復合點擊事件進行點擊
    if (
      !event.defaultPrevented && // onClick prevented default
      event.button === 0 && // ignore everything but left clicks
      !this.props.target && // let browser handle "target=_blank" etc.
      !isModifiedEvent(event) // ignore clicks with modifier keys
    ) {
      event.preventDefault(); // 必須要阻止默認事件,否則會走a標簽href屬性里的地址

      const { history } = this.context.router;
      const { replace, to } = this.props;
        // 進行跳轉
      if (replace) {
        history.replace(to);
      } else {
        history.push(to);
      }
    }
  };

  render() {
    const { replace, to, innerRef, ...props } = this.props; // eslint-disable-line no-unused-vars

    invariant(
      this.context.router,
      "You should not use  outside a "
    );
    // 必須指定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;
9. NavLink.js

NavLink.js 是Link.js的升級版,主要功能就是對Link添加了激活狀態,方便進行導航樣式的控制。這里我們可以設想下如何實現這個功能?可以使用Link傳遞的to參數,生成一個路徑然后和當前地址欄的pathname進行匹配,匹配成功的給Link添加activeClass即可。其實NavLink也是這樣實現的。參數如下:

① to: 即Link當中to,即將跳轉的地址,這里還用來進行正則匹配

② exact: boolean, 默認false, 即正則匹配到的url是否完全和地址欄pathname相等

③ strict: boolean, 默認false, 即最后的 ‘/’ 是否加入匹配

④ location: object, 自定義的location匹配對象

⑤ activeClassName: string, 即當Link被激活時候的class名稱

⑥ className: string, 對Link的改寫的class名稱

⑦ activeStyle: object, Link被激活時的樣式

⑧ style: object, 對Link改寫的樣式

⑨ isAcitve: func, 當Link被匹配到的時候的回調函數,可以再此對匹配到LInk進行自定義的業務邏輯,當返回false時,Link樣式也不會被激活

⑩ aria-current: string, 當Link被激活時候的html自定義屬性

源碼如下:
import React from "react";
import PropTypes from "prop-types";
import Route from "./Route";
import Link from "./Link";

const NavLink = ({
  to,
  exact,
  strict,
  location,
  activeClassName,
  className,
  activeStyle,
  style,
  isActive: getIsActive,
  "aria-current": ariaCurrent,
  ...rest
}) => {
  const path = typeof to === "object" ? to.pathname : to;
  // 看到這里的時候會有一個疑問,為什么要將path里面的特殊符號轉義
  // 在Switch里一樣有對Route Redirect進行劫持的操作,并沒有將里面的path進行此操作,
  // Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
  const escapedPath = path && path.replace(/([.+*?=^!:${}()[]|/])/g, "$1");
    
  return (
     {
        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 v4.x 源碼拾遺1

    摘要:還是先來一段官方的基礎使用案例,熟悉一下整體的代碼流程中使用了端常用到的等一些常用組件,作為的頂層組件來獲取的和設置回調函數來更新。 react-router是react官方推薦并參與維護的一個路由庫,支持瀏覽器端、app端、服務端等常見場景下的路由切換功能,react-router本身不具備切換和跳轉路由的功能,這些功能全部由react-router依賴的history庫完成,his...

    itvincent 評論0 收藏0
  • react-router v4.x 源碼拾遺1

    摘要:還是先來一段官方的基礎使用案例,熟悉一下整體的代碼流程中使用了端常用到的等一些常用組件,作為的頂層組件來獲取的和設置回調函數來更新。 react-router是react官方推薦并參與維護的一個路由庫,支持瀏覽器端、app端、服務端等常見場景下的路由切換功能,react-router本身不具備切換和跳轉路由的功能,這些功能全部由react-router依賴的history庫完成,his...

    Joyven 評論0 收藏0
  • react-router v4.x 源碼拾遺2

    摘要:如果將添加到當前組件,并且當前組件由包裹,那么將引用最外層包裝組件的實例而并非我們期望的當前組件,這也是在實際開發中為什么不推薦使用的原因,使用一個回調函數是一個不錯的選擇,也同樣的使用的是回調函數來實現的。 回顧:上一篇講了BrowserRouter 和 Router之前的關系,以及Router實現路由跳轉切換的原理。這一篇來簡短介紹react-router剩余組件的源碼,結合官方文...

    luoyibu 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<