前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >react-router/v5之Router、Route、Redirect、Switch源码解析

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

原创
作者头像
用户4619307
修改2022-09-28 10:03:59
1K0
修改2022-09-28 10:03:59
举报
文章被收录于专栏:TangPieceTangPiece

前言

本文是基于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>

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 概述
  • 特别说明
  • 源码解析
    • Router组件
      • Route组件
        • matchPath:路由匹配方法
          • Redirect组件
            • Switch组件
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档