react-router v4.x 源码拾遗2

栏目: IOS · Android · 发布时间: 6年前

内容简介:回顾:上一篇讲了BrowserRouter 和 Router之前的关系,以及Router实现路由跳转切换的原理。这一篇来简短介绍react-router剩余组件的源码,结合官方文档,一起探究实现的的方式。Switch对props.chidlren做遍历筛选,将第一个与pathname匹配到的Route或者Redirect进行渲染(此处只要包含有path这个属性的子节点都会进行筛选,所以可以直接使用自定义的组件,如果缺省path这个属性,并且当匹配到这个子节点时,那么这个子节点就会被渲染同时筛选结束,即Swi

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


以上所述就是小编给大家介绍的《react-router v4.x 源码拾遗2》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web 2.0 Heroes

Web 2.0 Heroes

Bradley L. Jones / Wiley / 2008-04-14 / USD 24.99

Web 2.0 may be an elusive concept, but one thing is certain: using the Web as merely a means of retrieving and displaying information is history. Today?s Web is immediate, interactive, innovative. It ......一起来看看 《Web 2.0 Heroes》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试