react-router/v5之Router、Route、Redirect、Switch源码解析

2022-09-28 10:03:59 浏览数 (3)

前言

本文是基于react-router的v5版本(v5.3.3),不适用最新的v6版本

参考文章:手写React-Router源码,深入理解其原理

概述

首先,简单概括一下RouterRouteRedirectSwitch的作用:

Router:创建一个context上下文对象,并注入historylocationmatch等全局变量。BrowerRouterHashRouter只是调用了history不同的方法

Route:创建一个组件,当前路由与其path匹配时,返回对应的组件,否则返回null。

Redirect:创建一个组件,只要组件被挂载或更新时,就会执行重定向。注意,这个组件内部是不进行路由匹配的

SwitchSwitch的作用是循环遍历子节点children数组,依次和当前路由进行匹配,只要匹配到就不再进行匹配,返回匹配到的路由。

特别说明

1、Route内部进行的路由匹配是独立的,也就是如果有多个Route同时和当前路由匹配,会把所有匹配到的路由都渲染,Switch的作用就是控制Route只匹配一次。

2、Redirect本身是不进行路由匹配的,所以需要依赖Switch的路由匹配逻辑。也就是说,使用Redirect时必须使用Switch作为父节点。

3、Switch进行路由匹配时,遍历的子节点只是一级子节点,并不会去遍历孙节点,且遍历子节点的顺序是RouteRedirectjsx中从上到下的顺序。所以,RouteRedirect只能作为Switch的一级子节点,如果有嵌套路由,每级路由都需要加上Switch

源码解析

了解了基本原理,我们结合源码解析一下

Router组件

代码语言:jsx复制
class Router extends React.Component {
  // 创建match对象
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);
    // ... 定义属性
  }

  componentDidMount() {
    //... 做一些初始化
  }

  componentWillUnmount() {
    //... 组件销毁重置属性值
  }

  render() {
    return (
      // 1、返回context上下文
      <RouterContext.Provider
        // 2、注入history、location、match等全局变量
        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>
    );
  }
}

可以看出,Router最重要的作用就是注入了注入historylocationmatch等全局变量,以便在所有组件中都可以使用

Route组件

代码语言:javascript复制
class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          // 1、<Route>必须在<Router>内部
          invariant(context, "You should not use <Route> outside a <Router>");

          const location = this.props.location || context.location;
          
          // 2、通过matchPath方法将path值和当前路由进行匹配,如果<Switch>中已经匹配过,直接使用匹配结果
          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;
          
          // ... 

          return (
            <RouterContext.Provider value={props}>
              {
                // 3、如果匹配当前路由,就渲染children或component或render(),否则返回null
                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>
    );
  }
}

注释2处的matchPath是路由匹配的关键方法,Switch也是使用该方法进行的匹配。我们来看一下它的实现

matchPath:路由匹配方法

代码语言:javascript复制
// 1、可以看到,路由匹配使用的是path-to-regexp
import pathToRegexp from "path-to-regexp";

// ... 定义缓存变量

function compilePath(path, options) {
  // ... 读取缓存

  const keys = [];
  // 2、使用pathToRegexp获取匹配正则表达式
  const regexp = pathToRegexp(path, keys, options);
  const result = { regexp, keys };

  // ... 存入缓存

  return result;
}

function matchPath(pathname, options = {}) {
  // 将路径值统一放到options.path中
  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;
    // 3、只要匹配到就直接返回匹配结果
    if (matched) return matched;
    
    // 4、获取路由匹配正则表达式
    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive
    });
    // 5、使用正则进行路由匹配
    const match = regexp.exec(pathname);
    // 6、没有匹配结果返回null
    if (!match) return null;

    const [url, ...values] = match;
    const isExact = pathname === url;
    // 7、如果设置了exact,进行全匹配
    if (exact && !isExact) return null;

    // 8、匹配结果
    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);
}

可以看到,路径匹配实际使用的是path-to-regexp

下面我们也可以看到,Redirect中并没有路由匹配的逻辑。

Redirect组件

代码语言:javascript复制
function Redirect({ computedMatch, to, push = false }) {
  return (
    <RouterContext.Consumer>
      {context => {
        // 1、<Redirect>必须在<Router>中
        invariant(context, "You should not use <Redirect> outside a <Router>");

        const { history, staticContext } = context;
        // 2、重定向跳转的方式
        const method = push ? history.push : history.replace;
        // 3、使用to属性创建路由对象
        const location = createLocation(
          computedMatch
            ? typeof to === "string"
              ? generatePath(to, computedMatch.params)
              : {
                  ...to,
                  pathname: generatePath(to.pathname, computedMatch.params)
                }
            : to
        );

        // ... staticContext非空时的处理,可先忽略

        return (
          // 4、Lifecycle组件就是一个简单的class组件,组件挂载时回调onMount,更新时回调onUpdate
          <Lifecycle
            onMount={() => {
              // 5、Redirect组件只要挂载时就会执行路由跳转方法
              method(location);
            }}
            onUpdate={(self, prevProps) => {
              // 6、需要更新的to路由信息
              const prevLocation = createLocation(prevProps.to);
              // 7、如果新的to路由和旧的to路由不相等,则进行重定向,避免死循环
              if (
                !locationsAreEqual(prevLocation, {
                  ...location,
                  key: prevLocation.key
                })
              ) {
                // 8、执行路由跳转方法
                method(location);
              }
            }}
            to={to}
          />
        );
      }}
    </RouterContext.Consumer>
  );
}

通过注释5可见,Redirect组件只要挂载就会进行重定向,所以需要通过Switch进来路由匹配控制

Switch组件

代码语言:javascript复制
class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          // 1、<Switch>必须在<Router>中
          invariant(context, "You should not use <Switch> outside a <Router>");

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

          let element, match;

          // 2、遍历Switch的子节点children
          React.Children.forEach(this.props.children, child => {
            // 3、如果还没有匹配到结果
            if (match == null && React.isValidElement(child)) {
              element = child;
              
              // 4、获取Route的path、或Redirect的from属性值
              const path = child.props.path || child.props.from;
              
              // 5、使用matchPath将子节点的路径和当前路径进行匹配
              match = path
                ? matchPath(location.pathname, { ...child.props, path })
                : context.match;
            }
          });
          
          // 6、如果匹配到,返回对应的子节点;如果没有匹配到,返回null
          return match
            ? React.cloneElement(element, { location, computedMatch: match })
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}

从注释3处可以看到,Switch通过macth变量控制只要匹配到立即返回匹配到的路由。所以,需要注意RouteRedirect组件的顺序,特别是通过*做404重定向时,必须将其他所有RouteRedirect组件放到*路由之前

代码语言:javascript复制
<Switch>
    // ... 其他Route和Redirect组件必须放前面
    <Redirect from="*" to="/" />
</Switch>

1 人点赞