React Router v5.2.0 源码之 react-router 包讲解

240 阅读3分钟

欢迎关注微信公众号:前端阅读室

架构

history 库

框架无关,router 的核心逻辑(这次只简单介绍)

react-router 库

有 4 个包,使用 lerna 管理:

react-router

使用 React 框架基于 history 实现的 router 状态管理。dom 无关,只负责管理状态。

react-router-dom

dom 相关,由于前面(react-router 包)状态管理已经实现好了,所以 react-router-dom 包的代码量很少,只需要基于react-router 包的实现封装下。

react-router-config

Static route configuration helpers for React Router。代码量很少,没用过,不讲解。

react-router-native

没看过,有 android 和 ios 相关的代码,没做过相关业务

所以,本次分享主要介绍 react-router。

预备知识

  1. 前端基础:history、location

  2. react: refs 转发、context

  3. 依赖库:

    history(^4.9.0)

    path-to-regexp(^1.7.0):主要用到 pathToRegexp.compile(path)、pathToRegexp(path, keys, options) 这两个方法:

    - pathToRegexp.compile:transforming parameters into a valid path
    
    - pathToRegexp 将路径字符串转换为正则表达式
    

铺垫知识:history 库实现的简单介绍

前言

history 库是 react-router 依赖的核心库,它将应用的 history 做了统一的封装,包含一系列统一的属性和方法,支持浏览器的 BrowserHistory、HashHistory 以及服务端的 MemoryHistory。

下面是 3 种 history 的属性和方法,常见的有比如 push、go 等。

createBrowserHistory 的属性和方法

length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen

createHashHistory 的属性和方法

length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen

createMemoryHistory 的属性和方法

length: entries.length,
action: 'POP',
location: entries[index],
index,
entries,
createHref,
push,
replace,
go,
goBack,
goForward,
canGo,
block,
listen

下面我们介绍下 history.listen 和 history.push 在 browserHistory 中的实现。

history.listen

history.listen 在浏览器中使用 DOM 方法进行事件的监听和移除。

browserHistory 中的实现

browserHistory 使用的是 popstate 和 hashchange 事件。

同时会将事件触发的回调函数添加到 transitionManager 中,这样事件触发时只需要通过执行 transitionManager.notifyListeners() 发送通知,就可以执行这些回调函数了。

const PopStateEvent = "popstate";
const HashChangeEvent = "hashchange";

let listenerCount = 0;

function checkDOMListeners(delta) {
  listenerCount += delta;

  if (listenerCount === 1 && delta === 1) {
    window.addEventListener(PopStateEvent, handlePopState);

    if (needsHashChangeListener)
      window.addEventListener(HashChangeEvent, handleHashChange);
  } else if (listenerCount === 0) {
    window.removeEventListener(PopStateEvent, handlePopState);

    if (needsHashChangeListener)
      window.removeEventListener(HashChangeEvent, handleHashChange);
  }
}

function listen(listener) {
  const unlisten = transitionManager.appendListener(listener);
  checkDOMListeners(1);

  return () => {
    checkDOMListeners(-1);
    unlisten();
  };
}

由于有的浏览器不支持 popstate,所以使用 hashchange 事件监听。

const needsHashChangeListener = !supportsPopStateOnHashChange();

export function supportsPopStateOnHashChange() {
  return window.navigator.userAgent.indexOf("Trident") === -1;
}

history.push

browserHistory 中的实现

history.push 方法很简单,主要调用了 history.pushState 方法。由于 allKeys 维护了所有 history state 中的 key,所以需要在 push 方法中做相应的处理。

const globalHistory = window.history;

function push(path, state) {
  warning(
    !(
      typeof path === "object" &&
      path.state !== undefined &&
      state !== undefined
    ),
    "You should avoid providing a 2nd state argument to push when the 1st " +
      "argument is a location-like object that already has state; it is ignored"
  );

  const action = "PUSH";
  const location = createLocation(path, state, createKey(), history.location);

  transitionManager.confirmTransitionTo(
    location,
    action,
    getUserConfirmation,
    (ok) => {
      if (!ok) return;

      const href = createHref(location);
      const { key, state } = location;

      if (canUseHistory) {
        globalHistory.pushState({ key, state }, null, href);

        if (forceRefresh) {
          window.location.href = href;
        } else {
          const prevIndex = allKeys.indexOf(history.location.key);
          const nextKeys = allKeys.slice(
            0,
            prevIndex === -1 ? 0 : prevIndex + 1
          );

          nextKeys.push(location.key);
          allKeys = nextKeys;

          setState({ action, location });
        }
      } else {
        warning(
          state === undefined,
          "Browser history cannot push state in browsers that do not support HTML5 history"
        );

        window.location.href = href;
      }
    }
  );
}

react-router 前言

history 库需要大篇幅介绍,它是路由管理的底层逻辑。react-router 其实只是使用 react 框架封装了 history 库的处理(主要使用 context 跨组件传递 history 的状态和方法)。

现在假设我们已经实现了 history 库,你能不能大致勾勒出诸如 <BrowserRouter><Route><Switch><Link>withRouter() ... 的简单实现呢?

下面让我们来看看 react-router 具体是怎么实现的。

react-router 前菜

createNamedContext()

该方法可以创建有 displayName 的 context。

// TODO: Replace with React.createContext once we can assume React 16+
import createContext from "mini-create-react-context";

const createNamedContext = (name) => {
  const context = createContext();
  context.displayName = name;

  return context;
};

export default createNamedContext;

generatePath()

生成路径,主要调用的是 pathToRegexp.compile()方法,generatePath 可以根据路径 path 和参数 params 生成完整的路径。比如 ('/a/:id', { id: 1 }) -> '/a/1'

import pathToRegexp from "path-to-regexp";

const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;

function compilePath(path) {
  if (cache[path]) return cache[path];

  const generator = pathToRegexp.compile(path);

  if (cacheCount < cacheLimit) {
    cache[path] = generator;
    cacheCount++;
  }

  return generator;
}

/**
 * Public API for generating a URL pathname from a path and parameters.
 */
function generatePath(path = "/", params = {}) {
  return path === "/" ? path : compilePath(path)(params, { pretty: true });
}

export default generatePath;

matchPath()

该方法传入 pathname,以及解析 pathname 的配置,可以得到从 pathname 中匹配的结果。

我们在使用 react-router 时经常会见到下面数据,它就是通过 matchPath 方法解析的。

return {
  path, // the path 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) => {
    memo[key.name] = values[index];
    return memo;
  }, {}),
};

matchPath() 的实现:

import pathToRegexp from "path-to-regexp";

const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;

function compilePath(path, options) {
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {});

  if (pathCache[path]) return pathCache[path];

  const keys = [];
  const regexp = pathToRegexp(path, keys, options);
  const result = { regexp, keys };

  if (cacheCount < cacheLimit) {
    pathCache[path] = result;
    cacheCount++;
  }

  return result;
}

/**
 * Public API for matching a URL pathname to a path.
 */
function matchPath(pathname, options = {}) {
  if (typeof options === "string" || Array.isArray(options)) {
    options = { path: options };
  }

  const { path, exact = false, strict = false, sensitive = false } = options;

  const paths = [].concat(path);

  return paths.reduce((matched, path) => {
    if (!path && path !== "") return null;
    if (matched) return matched;

    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive,
    });
    const match = regexp.exec(pathname);

    if (!match) return null;

    const [url, ...values] = match;
    const isExact = pathname === url;

    if (exact && !isExact) return null;

    return {
      path, // the path 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) => {
        memo[key.name] = values[index];
        return memo;
      }, {}),
    };
  }, null);
}

export default matchPath;

historyContext

创建 historyContext。

import createNamedContext from "./createNameContext";

const historyContext = /*#__PURE__*/ createNamedContext("Router-History");
export default historyContext;

routerContext

创建 routerContext。这里源码的写法有冗余了。

// TODO: Replace with React.createContext once we can assume React 16+
import createContext from "mini-create-react-context";

const createNamedContext = (name) => {
  const context = createContext();
  context.displayName = name;

  return context;
};

const context = /*#__PURE__*/ createNamedContext("Router");
export default context;

Lifecycle

创建一个 react 组件,它是一个空组件,主要是为了方便在组件生命周期的一些阶段调用用户通过 props 传入的回调函数。

import React from "react";

class Lifecycle extends React.Component {
  componentDidMount() {
    if (this.props.onMount) this.props.onMount.call(this, this);
  }

  componentDidUpdate(prevProps) {
    if (this.props.onUpdate) this.props.onUpdate.call(this, this, prevProps);
  }

  componentWillUnmount() {
    if (this.props.onUnmount) this.props.onUnmount.call(this, this);
  }

  render() {
    return null;
  }
}

export default Lifecycle;

react-router 正菜

Router

<Router>是我们很常用的组件,有了前面的知识铺垫,理解它就很简单啦。

Router 组件内部会维护 location 的 state,如果不是静态路由,通过 history.listen 方法监听 history 的变化。这里的 history 就是我们前面介绍的 history 库生成的 history,它可以使用 browserHistory、hashHistory、memoryHistory 三种 history,history 库对这三种 history 做了统一的接口封装。history 如果发生改变,会调用this.setState({ location }),组件重新渲染,RouterContext.Provider、HistoryContext.Provider 的值更新,它们的子组件会感知到,从而获得最新的参数和方法。

import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";

import HistoryContext from "./HistoryContext.js";
import RouterContext from "./RouterContext.js";

/**
 * The public API for putting history on context.
 */
class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    this.state = {
      location: props.history.location,
    };

    this._isMounted = false;
    this._pendingLocation = null;

    if (!props.staticContext) {
      this.unlisten = props.history.listen((location) => {
        if (this._isMounted) {
          this.setState({ location });
        } else {
          this._pendingLocation = location;
        }
      });
    }
  }

  componentDidMount() {
    this._isMounted = true;

    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation });
    }
  }

  componentWillUnmount() {
    if (this.unlisten) this.unlisten();
  }

  render() {
    return (
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext,
        }}
      >
        <HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        />
      </RouterContext.Provider>
    );
  }
}

export default Router;

Route

使用 RouterContext.Consumer 可以感知到上层 RouterContext.Provider 值的变动,从而自动计算 "match",根据 "match" 的结果渲染匹配的组件(使用 props 传入 children, component, render 之一)。

如果有 computedMatch 属性说明在 <Switch> 组件中已经计算好了 match,可以直接使用。Switch 组件我们后面会介绍。

import React from "react";
import { isValidElementType } from "react-is";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import warning from "tiny-warning";

import RouterContext from "./RouterContext.js";
import matchPath from "./matchPath.js";

function isEmptyChildren(children) {
  return React.Children.count(children) === 0;
}

function evalChildrenDev(children, props, path) {
  const value = children(props);

  warning(
    value !== undefined,
    "You returned `undefined` from the `children` function of " +
      `<Route${path ? ` path="${path}"` : ""}>, but you ` +
      "should have returned a React element or `null`"
  );

  return value || null;
}

/**
 * The public API for matching a single path and rendering.
 */
class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {(context) => {
          invariant(context, "You should not use <Route> outside a <Router>");

          const location = this.props.location || context.location;
          const match = this.props.computedMatch
            ? this.props.computedMatch // <Switch> already computed the match for us
            : this.props.path
            ? matchPath(location.pathname, this.props)
            : context.match;

          const props = { ...context, location, match };

          let { children, component, render } = this.props;

          // Preact uses an empty array as children by
          // default, so use null if that's the case.
          if (Array.isArray(children) && children.length === 0) {
            children = null;
          }

          return (
            <RouterContext.Provider value={props}>
              {props.match
                ? children
                  ? typeof children === "function"
                    ? __DEV__
                      ? evalChildrenDev(children, props, this.props.path)
                      : children(props)
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? __DEV__
                  ? evalChildrenDev(children, props, this.props.path)
                  : children(props)
                : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

export default Route;

Redirect

重定向组件根据传入的 push 属性可以决定使用 history.push 还是 history.replace 进行重定向,根据传入 computedMatch, to 可以计算出重定向的 location。如果在静态组件中,会直接执行重定向。如果不是,会使用空组件 Lifecycle,在组件挂载阶段重定向,在 onUpdate 中判断重定向是否完成。

import React from "react";
import PropTypes from "prop-types";
import { createLocation, locationsAreEqual } from "history";
import invariant from "tiny-invariant";

import Lifecycle from "./Lifecycle.js";
import RouterContext from "./RouterContext.js";
import generatePath from "./generatePath.js";

/**
 * The public API for navigating programmatically with a component.
 */
function Redirect({ computedMatch, to, push = false }) {
  return (
    <RouterContext.Consumer>
      {(context) => {
        invariant(context, "You should not use <Redirect> outside a <Router>");

        const { history, staticContext } = context;

        const method = push ? history.push : history.replace;
        const location = createLocation(
          computedMatch
            ? typeof to === "string"
              ? generatePath(to, computedMatch.params)
              : {
                  ...to,
                  pathname: generatePath(to.pathname, computedMatch.params),
                }
            : to
        );

        // When rendering in a static context,
        // set the new location immediately.
        if (staticContext) {
          method(location);
          return null;
        }

        return (
          <Lifecycle
            onMount={() => {
              method(location);
            }}
            onUpdate={(self, prevProps) => {
              const prevLocation = createLocation(prevProps.to);
              if (
                !locationsAreEqual(prevLocation, {
                  ...location,
                  key: prevLocation.key,
                })
              ) {
                method(location);
              }
            }}
            to={to}
          />
        );
      }}
    </RouterContext.Consumer>
  );
}

export default Redirect;

Switch

被 Switch 组件包裹的组件只会渲染其中第一个路由匹配成功的组件。

主要通过 React.Children.forEach(this.props.children, child => {}),遍历出第一个匹配的路由及组件,再使用 React.cloneElement 返回这个组件。

import React from "react";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import warning from "tiny-warning";

import RouterContext from "./RouterContext.js";
import matchPath from "./matchPath.js";

/**
 * The public API for rendering the first <Route> that matches.
 */
class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {(context) => {
          invariant(context, "You should not use <Switch> outside a <Router>");

          const location = this.props.location || context.location;

          let element, match;

          // We use React.Children.forEach instead of React.Children.toArray().find()
          // here because toArray adds keys to all child elements and we do not want
          // to trigger an unmount/remount for two <Route>s that render the same
          // component at different URLs.
          React.Children.forEach(this.props.children, (child) => {
            if (match == null && React.isValidElement(child)) {
              element = child;

              const path = child.props.path || child.props.from;

              match = path
                ? matchPath(location.pathname, { ...child.props, path })
                : context.match;
            }
          });

          return match
            ? React.cloneElement(element, { location, computedMatch: match })
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}

export default Switch;

StaticRouter

静态路由组件自己实现了一个简单的 history,没有监听 history 变化的概念,也不需要 go、goBack、goForward、listen、block 方法。

import React from "react";
import PropTypes from "prop-types";
import { createLocation, createPath } from "history";
import invariant from "tiny-invariant";
import warning from "tiny-warning";

import Router from "./Router.js";

function addLeadingSlash(path) {
  return path.charAt(0) === "/" ? path : "/" + path;
}

function addBasename(basename, location) {
  if (!basename) return location;

  return {
    ...location,
    pathname: addLeadingSlash(basename) + location.pathname,
  };
}

function stripBasename(basename, location) {
  if (!basename) return location;

  const base = addLeadingSlash(basename);

  if (location.pathname.indexOf(base) !== 0) return location;

  return {
    ...location,
    pathname: location.pathname.substr(base.length),
  };
}

function createURL(location) {
  return typeof location === "string" ? location : createPath(location);
}

function staticHandler(methodName) {
  return () => {
    invariant(false, "You cannot %s with <StaticRouter>", methodName);
  };
}

function noop() {}

/**
 * The public top-level API for a "static" <Router>, so-called because it
 * can't actually change the current location. Instead, it just records
 * location changes in a context object. Useful mainly in testing and
 * server-rendering scenarios.
 */
class StaticRouter extends React.Component {
  navigateTo(location, action) {
    const { basename = "", context = {} } = this.props;
    context.action = action;
    context.location = addBasename(basename, createLocation(location));
    context.url = createURL(context.location);
  }

  handlePush = (location) => this.navigateTo(location, "PUSH");
  handleReplace = (location) => this.navigateTo(location, "REPLACE");
  handleListen = () => noop;
  handleBlock = () => noop;

  render() {
    const { basename = "", context = {}, location = "/", ...rest } = this.props;

    const history = {
      createHref: (path) => addLeadingSlash(basename + createURL(path)),
      action: "POP",
      location: stripBasename(basename, createLocation(location)),
      push: this.handlePush,
      replace: this.handleReplace,
      go: staticHandler("go"),
      goBack: staticHandler("goBack"),
      goForward: staticHandler("goForward"),
      listen: this.handleListen,
      block: this.handleBlock,
    };

    return <Router {...rest} history={history} staticContext={context} />;
  }
}

export default StaticRouter;

MemoryRouter

MemoryRouter 的 history 指定使用了 createMemoryHistory,内部逻辑就是 Router 的逻辑。

import React from "react";
import PropTypes from "prop-types";
import { createMemoryHistory as createHistory } from "history";
import warning from "tiny-warning";

import Router from "./Router.js";

/**
 * The public API for a <Router> that stores location in memory.
 */
class MemoryRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

export default MemoryRouter;

withRouter

由于从 RouterContext.Consumer 的 context 中可以很方便的获取到路由参数,所以 withRouter 就很容易实现了。只需要使用高阶组件,接收需要获取路由参数的组件作为参数,将 context 作为参数传入该组件即可。

import React from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";
import invariant from "tiny-invariant";

import RouterContext from "./RouterContext.js";

/**
 * A public higher-order component to access the imperative API
 */
function withRouter(Component) {
  const displayName = `withRouter(${Component.displayName || Component.name})`;
  const C = (props) => {
    const { wrappedComponentRef, ...remainingProps } = props;

    return (
      <RouterContext.Consumer>
        {(context) => {
          invariant(
            context,
            `You should not use <${displayName} /> outside a <Router>`
          );
          return (
            <Component
              {...remainingProps}
              {...context}
              ref={wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  };

  C.displayName = displayName;
  C.WrappedComponent = Component;

  return hoistStatics(C, Component);
}

export default withRouter;

hooks

react-router 还提供了一些获取 history、location、路由参数...的 react hooks。

import React from "react";
import invariant from "tiny-invariant";

import Context from "./RouterContext.js";
import HistoryContext from "./HistoryContext.js";
import matchPath from "./matchPath.js";

const useContext = React.useContext;

export function useHistory() {
  return useContext(HistoryContext);
}

export function useLocation() {
  return useContext(Context).location;
}

export function useParams() {
  const match = useContext(Context).match;
  return match ? match.params : {};
}

export function useRouteMatch(path) {
  const location = useLocation();
  const match = useContext(Context).match;

  return path ? matchPath(location.pathname, path) : match;
}

react-router-dom

react-router 工程还包括了 react-router-dom 库的实现,用来提供 dom 相关的路由操作。

我们在 react 工程中很多时候使用的是 react-router-dom 库,它的底层是前面介绍的 react-router。

BrowserRouter

我们在项目中使用 HTML5 history 控制路由,可以直接使用 react-router-dom 的 BrowserRouter。

import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";

/**
 * The public API for a <Router> that uses HTML5 history.
 */
class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

export default BrowserRouter;

HashRouter

我们在项目中使用 location hash 控制路由,可以直接使用 react-router-dom 的 HashRouter。

import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";

/**
 * The public API for a <Router> that uses window.location.hash.
 */
class HashRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

export default HashRouter;

Link

<Link> 组件是 react-router 中常见的路由跳转组件。它使用的是 html 的 a 标签,为其绑定了点击事件。用户点击时,既可以执行用户自定义的 onClick 回调函数,也会执行 navigate -> method(location),method 可以根据用户传入的 replace 参数决定是使用 history.replace 还是 history.push,同时点击事件会阻止事件冒泡。

Link 还暴露了 forwardedRef 属性,可以转发 ref。

import React from "react";
import { __RouterContext as RouterContext } from "react-router";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import {
  resolveToLocation,
  normalizeToLocation,
} from "./utils/locationUtils.js";

// React 15 compat
const forwardRefShim = (C) => C;
let { forwardRef } = React;
if (typeof forwardRef === "undefined") {
  forwardRef = forwardRefShim;
}

function isModifiedEvent(event) {
  return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}

const LinkAnchor = forwardRef(
  (
    {
      innerRef, // TODO: deprecate
      navigate,
      onClick,
      ...rest
    },
    forwardedRef
  ) => {
    const { target } = rest;

    let props = {
      ...rest,
      onClick: (event) => {
        try {
          if (onClick) onClick(event);
        } catch (ex) {
          event.preventDefault();
          throw ex;
        }

        if (
          !event.defaultPrevented && // onClick prevented default
          event.button === 0 && // ignore everything but left clicks
          (!target || target === "_self") && // let browser handle "target=_blank" etc.
          !isModifiedEvent(event) // ignore clicks with modifier keys
        ) {
          event.preventDefault();
          navigate();
        }
      },
    };

    // React 15 compat
    if (forwardRefShim !== forwardRef) {
      props.ref = forwardedRef || innerRef;
    } else {
      props.ref = innerRef;
    }

    /* eslint-disable-next-line jsx-a11y/anchor-has-content */
    return <a {...props} />;
  }
);

if (__DEV__) {
  LinkAnchor.displayName = "LinkAnchor";
}

/**
 * The public API for rendering a history-aware <a>.
 */
const Link = forwardRef(
  (
    {
      component = LinkAnchor,
      replace,
      to,
      innerRef, // TODO: deprecate
      ...rest
    },
    forwardedRef
  ) => {
    return (
      <RouterContext.Consumer>
        {(context) => {
          invariant(context, "You should not use <Link> outside a <Router>");

          const { history } = context;

          const location = normalizeToLocation(
            resolveToLocation(to, context.location),
            context.location
          );

          const href = location ? history.createHref(location) : "";
          const props = {
            ...rest,
            href,
            navigate() {
              const location = resolveToLocation(to, context.location);
              const method = replace ? history.replace : history.push;

              method(location);
            },
          };

          // React 15 compat
          if (forwardRefShim !== forwardRef) {
            props.ref = forwardedRef || innerRef;
          } else {
            props.innerRef = innerRef;
          }

          return React.createElement(component, props);
        }}
      </RouterContext.Consumer>
    );
  }
);

export default Link;

NavLink

A special version of the <Link> that will add styling attributes to the rendered element when it matches the current URL.

import React from "react";
import { __RouterContext as RouterContext, matchPath } from "react-router";
import PropTypes from "prop-types";
import invariant from "tiny-invariant";
import Link from "./Link.js";
import {
  resolveToLocation,
  normalizeToLocation,
} from "./utils/locationUtils.js";

// React 15 compat
const forwardRefShim = (C) => C;
let { forwardRef } = React;
if (typeof forwardRef === "undefined") {
  forwardRef = forwardRefShim;
}

function joinClassnames(...classnames) {
  return classnames.filter((i) => i).join(" ");
}

/**
 * A <Link> wrapper that knows if it's "active" or not.
 */
const NavLink = forwardRef(
  (
    {
      "aria-current": ariaCurrent = "page",
      activeClassName = "active",
      activeStyle,
      className: classNameProp,
      exact,
      isActive: isActiveProp,
      location: locationProp,
      sensitive,
      strict,
      style: styleProp,
      to,
      innerRef, // TODO: deprecate
      ...rest
    },
    forwardedRef
  ) => {
    return (
      <RouterContext.Consumer>
        {(context) => {
          invariant(context, "You should not use <NavLink> outside a <Router>");

          const currentLocation = locationProp || context.location;
          const toLocation = normalizeToLocation(
            resolveToLocation(to, currentLocation),
            currentLocation
          );
          const { pathname: path } = toLocation;
          // Regex taken from: https://github.com/pillarjs/path-to-regexp/blob/master/index.js#L202
          const escapedPath =
            path && path.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");

          const match = escapedPath
            ? matchPath(currentLocation.pathname, {
                path: escapedPath,
                exact,
                sensitive,
                strict,
              })
            : null;
          const isActive = !!(isActiveProp
            ? isActiveProp(match, currentLocation)
            : match);

          const className = isActive
            ? joinClassnames(classNameProp, activeClassName)
            : classNameProp;
          const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;

          const props = {
            "aria-current": (isActive && ariaCurrent) || null,
            className,
            style,
            to: toLocation,
            ...rest,
          };

          // React 15 compat
          if (forwardRefShim !== forwardRef) {
            props.ref = forwardedRef || innerRef;
          } else {
            props.innerRef = innerRef;
          }

          return <Link {...props} />;
        }}
      </RouterContext.Consumer>
    );
  }
);

export default NavLink;

欢迎关注微信公众号:前端阅读室