react-router v4.x 源码拾遗2

回忆:上一篇讲了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 <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;

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 <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;

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 (
      <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;

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 <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的值是不是相称

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 <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;

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 <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;

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 (
    <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了改掉就好

    原文作者:372563106
    原文地址: https://segmentfault.com/a/1190000019294068
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞