reactrouter-v4x-源码拾遗2

41次阅读

共计 23154 个字符,预计需要花费 58 分钟才能阅读完成。

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

正文完
 0