React-router4(新版)原始碼淺析

Kevin .ᘜ發表於2019-03-04

前言

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

不知道看官老爺有沒有注意到這裡

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}
            />
            )
        }
      
      ````

 }

複製程式碼

到這裡,本文結束,歡迎看官老爺們的到來。

相關文章