React-router4源码浅析

1,719 阅读7分钟

前言

router作为当前盛行的单页面应用必不可少的部分,今天我们就以React-Router V4为例,来解开他的神秘面纱。本文并不专注于讲解 Reacr-Router V4 的基础概念,可以前往官方文档了解更多基础知识

本文以RRV4代指Reacr-Router V4

预备知识

RRV4依赖的history

先来看几个问题

Q1. 为什么我们有时看到的写法是这样的

 import {
   Switch,
   Route, Router,
   BrowserRouter, Link
 } from 'react-router-dom';

或是这样的?

import {Switch, Route, Router} from 'react-router';
import {BrowserRouter, Link} from 'react-router-dom';

react-router-dom和react-router有什么关系和区别?

Q2. 为什么v4版本中支持div等标签的嵌套了?

Q3. Route 会在当前 url 与 path 属性值相符的时候渲染相关组件,他是如何做到的呢?

Q4. 为什么用Link组件,而不是a标签?

路由

进入RR V4之前,先想想路由的作用,路由的作用就是同步url与其对应的回调函数。一般基于history,通过history.pushstatereplacestate方法修改url,通过window.addEventListener('popstate', callback) 来监听前进后退,对于hash路由,通过window.location.hash修改hash,通过window.addEventListener('hashchange', callback) 监听变化

正文

为了便于理解原理, 先来一段关于RRV4的简单代码

   <BrowserRouter>
    <div>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
      </ul>

      <hr/>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </div>
  </BrowserRouter>

在看看Route的子组件的props

 {
    match: {
        path: "/",   // 用来匹配的 path
        url: "/",    // 当前的 URL
        params: {},  // 路径中的参数
        isExact:true     // 是否为严格匹配 pathname === "/" 
    },
    location: {
        hash: "" // hash
        key: "nyi4ea" // 唯一的key
        pathname: "/" // URL 中路径部分
        search: "" // URL 参数
        state: undefined // 路由跳转时传递的参数 state
    }
    history: {...}  // history库提供
    staticContext: undefined  // 用于服务端渲染
    
 }

我们带着问题去看源码,发现react-router-dom基于react-router,Router, Route, Switch等都是引用的react-router,并且加入了Link,BrowserRouter,HashRouter组件,这里解释了Q1,react-router负责通用的路由管理, react-router-dom负责web,当然还有react-router-native负责rn的管理,我们从BrowserRouter开始看

BrowserRouter

rrv4的作者提倡Just Components 概念,BrowserRouter很简单,以组件的形式包装了Router,history传递下去,当然HashRouter也是同理

BrowserRouter源码

import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";


class BrowserRouter extends React.Component {
  history = createHistory(this.props);

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

看完BrowserRouter,其实本质就是Router组件嘛,在下已经忍不住先去看react-router的Router源码了。

Router

Router作为Route的根组件,负责监听url的变化和传递数据(props), 这里使用了history.listen监听url,使用react context的Provider和Consumer模式,最初的数据来自history,并将 history, location, match, staticContext作为props传递

router源码+注释


// 构造props
function getContext(props, state) {
  return {
    history: props.history,
    location: state.location,
    match: Router.computeRootMatch(state.location.pathname),
    staticContext: props.staticContext
  };
}

class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    this.state = {
      // browserRouter的props为history
      location: props.history.location
    };
    this._isMounted = false;
    this._pendingLocation = null;

    // staticContext为true时,为服务器端渲染
    // staticContext为false
    if (!props.staticContext) {
      // 监听listen,location改变触发
      this.unlisten = props.history.listen(location => {
        // _isMounted为true表示经历过didmount,可以setState,防止在构造函数中setstate
        if (this._isMounted) {
          // 更新state location
          this.setState({ location });
        } else {
          // 否则存储到_pendingLocation, 等到didmount再setState避免可能报错
          this._pendingLocation = location;
        }
      });
    }
  }

  componentDidMount() {
    // 赋值为true,且不会再改变
    this._isMounted = true;
    // 更新location
    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation });
    }
  }

  componentWillUnmount() {
  // 取消监听
    if (this.unlisten) this.unlisten();
  }
  
  render() {
    const context = getContext(this.props, this.state);
    return (
      <RouterContext.Provider
        children={this.props.children || null}
        value={context}
      />
    );
  }
}

rrv4中Router组件为context中的Pirover, children可以是任何div等元素,这里解释了问题Q2,react-router的v4版本直接推翻了之前的v2,v3版本,在v2,v3的版本中Router组件根据子组件的Route,生成全局的路由表,路由表中记录了path与UI组件的映射关系,Router监听path变化,当path变化时,根据新的path找出对应所需的所有UI组件,按一定层级将这些UI渲染出来.而在rrv4中作者提倡Just Components思想,这也符合react中一切皆组件的思想。

Route

在v4中,Route只是一个Consumer包装的react组件,不管path是什么,Route组件总会渲染,在Route内部判断请求路径与当前的path是否匹配,匹配会继续渲染Route中的children或者component或者render中的子组件,如果不匹配,渲染null

route源码+注释


function getContext(props, context) {
  const location = props.location || context.location;
  const match = props.computedMatch
    ? props.computedMatch // <Switch> already computed the match for us
    : props.path  // <Route path='/xx' ... >
      ? matchPath(location.pathname, props)
      : context.match; // 默认 { path: "/", url: "/", params: {}, isExact: pathname === "/" }

  return { ...context, location, match };
}

class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Route> outside a <Router>");
          // 通过path生成props
          // this.props = {exact, path, component, children, render, computedMatch, ...others }
          // context = { history, location, staticContext, match }
          const props = getContext(this.props, context);
          // 结构Route的props
          let { children, component, render } = this.props;
          // 空数组用null代替
          if (Array.isArray(children) && children.length === 0) {
            children = null;
          }
          if (typeof children === "function") {
            // 无状态组件时
            children = children(props);
            if (children === undefined) {
              children = null;
            }
          }
          return (
            <RouterContext.Provider value={props}>
              {children && !isEmptyChildren(children) // children && React.Children.count > 0
                ? children  
                : props.match // match为true,查找到了匹配的<Route ... >
                  ? component
                    ? React.createElement(component, props) //创建react组件,传递props{ ...context, location, match }
                    : render
                      ? render(props) // 执行render方法
                      : null
                  : null}
            </RouterContext.Provider>
            // 优先级  children > component > render
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

Route是一个组件,每一个Route都会监听自己context并执行重新的渲染,为子组件提供了新的props, props.match用来决定是否渲染component和render,props.match由matchPath生成, 这里我们不得不看一下matchPath这个很重要的方法,他决定当前Route的path与url的匹配。

matchPath

matchPath方法依赖path-to-regexp库, 举个小🌰

  var pathToRegexp = require('path-to-regexp')
  var keys = []
  var re = pathToRegexp('/foo/:bar', keys)
  // re = /^\/foo\/([^\/]+?)\/?$/i
  // keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
  

matchPath源码+注释

/**
 * Public API for matching a URL pathname to a path.
 * @param {*} pathname  history.location.pathname
 * @param {*} options  
 * 默认配置,是否全局匹配 exact,末尾加/ strict, 大小写 sensitive, path <Route path="/xx" ...>
 */
function matchPath(pathname, options = {}) {
  // use <Switch />, options = location.pathname
  if (typeof options === "string") options = { path: options };
  const { path, exact = false, strict = false, sensitive = false } = options;
  // path存入paths数组
  const paths = [].concat(path);
  return paths.reduce((matched, path) => {
    if (matched) return matched;
    // compilePath内部使用path-to-regexp库,并做了缓存处理
    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive
    });
    // 在pathname中查找path
    const match = regexp.exec(pathname);
    // 匹配失败
    if (!match) return null;
    // 定义查找到的path为url
    const [url, ...values] = match;
    // 判断pathname与url是否相等 eg: '/' === '/home'
    const isExact = pathname === url;
    // 精准匹配时, 保证查找到的url === pathname
    if (exact && !isExact) return null;

    // 返回match object
    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);
}

Switch

不知道看官老爷有没有注意到这里

这就是Switch组件渲染与位置匹配的第一个子组件Route或Redirect的原因。Switch利用React.Children.forEach(this.props.children, child => {...})方法匹配第一个子组件, 如果匹配成功添加computedMatch props,props值为match。从而改变了matchPath的逻辑

switch部分源码

    class Switch extends React.Component {
       render() {
          ...省略无关代码
          let element, match;

          React.Children.forEach(this.props.children, child => {
            // child为react elemnet
            // match如果没有匹配到这为context.match
            if (match == null && React.isValidElement(child)) {
              element = child;

              // form用于<redirect form="..." ... >
              const path = child.props.path || child.props.from;

              // 匹配的match
              match = path
                ? matchPath(location.pathname, { ...child.props, path })
                : context.match; //  path undefind为默认mactch
                // note: path为undefined 时,会默认为'/'
            }
          });

          return match  // 添加computedMatch props为match
            ? React.cloneElement(element, { location, computedMatch: match })
            : null;
      }
    }
            ....

到这里我们了解了RRV4的基本工作流程和源码,解决了Q3,最后来看一下Link。

Link

Link组件的主要用于处理理用户通过点击锚标签进行跳转,之所以不用a标签是因为要避免每次用户切换路由时都进行页面的整体刷新,而是使用histoy库中的push和replace。解决Q4,当点击Link组件时,点击的是页面上渲染出来的 a 标签,通过preventDefault阻止默认行为,通过history的push或repalce跳转。

Link部分源码+注释

 class Link extends React.Component {
    ....
    
     handleClick(event, context) {
        if (this.props.onClick) this.props.onClick(event);
        if (
          !event.defaultPrevented && // onClick prevented default
          event.button === 0 && // 忽略不是左键的点击
          (!this.props.target || this.props.target === "_self") && // let browser handle "target=_blank" etc.
          !isModifiedEvent(event) // ignore clicks with modifier keys
        ) {
          // 阻止默认行为
          event.preventDefault();
    
          const method = this.props.replace
            ? context.history.replace
            : context.history.push;
    
          method(this.props.to);
        }
        ...
        
        render() {
          ....
          
            return(
              <a
              {...rest}
              onClick={event => this.handleClick(event, context)}
              href={href}
              ref={innerRef}
            />
            )
        }
      
      ''''

 }

到这里,本文结束,欢迎看官老爷们的到来。