React-router4(新版)源码浅析

栏目: IOS · Android · 发布时间: 7年前

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

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-touter有什么关系和区别?

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,通过 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 库, 举个小:chestnut:

// 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

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

React-router4(新版)源码浅析

这就是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}
            />
            )
        }
      
      ''''

 }

复制代码

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


以上所述就是小编给大家介绍的《React-router4(新版)源码浅析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

HTML5

HTML5

Matthew David / Focal Press / 2010-07-29 / USD 39.95

Implement the powerful new multimedia and interactive capabilities offered by HTML5, including style control tools, illustration tools, video, audio, and rich media solutions. Understand how HTML5 is ......一起来看看 《HTML5》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具