1. 程式人生 > >react-router原理

react-router原理

在一個web應用中,路由系統是不可或缺的一部分。許多前端框架都會有著不同的配套的路由系統,最近也開始接觸react,瞭解了一點react-router的實現原理。

hash路由

早期的前端路由是通過hash來實現的。改變url的hash值並不會重新整理頁面,因此就可以通過hash來實現前端路由。在當前的頁面中可以通過window.location.hash=‘test’,來改變當前頁面的hash值,執行後頁面的url會發生改變,但頁面並不會重新整理。
賦值前:http://localhost:8080 賦值後:http://localhost:8080/#test。
然後我們可以通過一個名為hashchange的事件來監聽頁面hash的變化。

window.addEventListener('hashchange',function(event){
   console.log(event);
})

有了這個監聽事件我們就可以在監聽事件的回撥函式中渲染新的UI,這樣就實現了一個簡單的路由功能。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <a href="#/a">pageALink</a> <a href="#/b">pageBLink</a> <span id='body'></span> <script> window.addEventListener
('hashchange',(e)=>{ document.getElementById('body').innerHTML = window.location },false)
</script> </body> </html>

history路由

history是H5的介面,允許我們操作瀏覽器會話歷史記錄。history提供了一些屬性和方法,主要包括:
屬性:

  • history.length:返回值為會話歷史中有多少條記錄,包含當前的會話頁面。
  • history.state:儲存了觸發popState事件的方法所傳遞的屬性物件。

方法:

  • History.back(): 返回瀏覽器會話歷史中的上一頁,跟瀏覽器的回退按鈕功能相同
  • History.forward():指向瀏覽器會話歷史中的下一頁,跟瀏覽器的前進按鈕相同。
  • History.go(): 可以跳轉到瀏覽器會話歷史中的指定的某一個記錄頁。
  • History.pushState():pushState可以將給定的資料壓入到瀏覽器會話歷史棧中,該方法接收3個引數,物件,title和一串url。pushState後會改變當前頁面url,但是不會伴隨著重新整理。
  • History.replaceState():replaceState將當前的會話頁面的url替換成指定的資料,replaceState後也會改變當前頁面的url,但是也不會重新整理頁面。

雖然pushState和replaceState都會改變當前頁面的url,並且不會重新整理頁面,但pushState是push一條新的記錄到瀏覽器的會話歷史棧中,會使history.length+1,而replaceState是替換當前這條會話歷史記錄,history.lenth並不會增加。
每次但我們觸發history.back(), History.forward()或點選瀏覽器的前進後退按鈕時,都會觸發popstate事件。
所以如果使用history做為路由基礎,就需要使用history.pushState和history.replaceState來改變url的值而不重新整理頁面,然後通過popstate事件執行頁面的前進與後退

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <a onClick="go('/a')">pageALink</a>
    <a onClick="go('/b')">pageBLink</a>
    <span id='body'></span>
    <script>
        function go (pathname){
            window.history.pushState({},null,pathname);
            document.getElementById('body').innerHTML = window.location;
        }
        
        //這裡主要處理瀏覽器前進後退功能,不加下面的程式碼就無法實現前進後退功能
        window.addEventListener('popstate',(e)=>{
            let pathname = window.location;
            document.getElementById('body').innerHTML = window.location;
        })
    </script>
</body>
</html>

react-router的基礎——history

History

history是一個第三方js庫,借鑑HTML5 history物件的理念,在其基礎上又擴充套件了一些功能,用來管理歷史記錄,可以相容不同的瀏覽器和不同的環境,根據不同的環境提供了三種不同的API。

  • HashHistory:針對老版本的瀏覽器,主要通過Hash實現。
  • BrowserHistory:針對較高版本的瀏覽器,主要通過H5的History實現。
  • MemoryHistory:主要通過記憶體中的歷史記錄實現。

History支援釋出/訂閱功能,當history發生改變的時候,可以自動觸發訂閱的函式,再對比一些兩者api的異同。以下是history庫的:

const history = {
    length,        // 屬性,history中記錄的state的數量	
    action,        // 屬性,當前導航的action型別
    location,      // 屬性,location物件,封裝了pathname、search和hash等屬性
    push,          // 方法,導航到新的路由,並記錄在history中
    replace,       // 方法,替換掉當前記錄在history中的路由資訊
    go,            // 方法,前進或後退n個記錄
    goBack,        // 方法,後退
    goForward,     // 方法,前進
    canGo,         // 方法,是否能前進或後退n個記錄
    block,         // 方法,跳轉前讓使用者確定是否要跳轉
    listen         // 方法,訂閱history變更事件
  };

以下是HTML5 history物件的:

const history = {
    length,         // 屬性,history中記錄的state的數量
    state,          // 屬性,pushState和replaceState時傳入的物件
    back,           // 方法,後退
    forward,        // 方法,前進
    go,             // 方法,前進或後退n個記錄
    pushState,      // 方法,導航到新的路由,並記錄在history中
    replaceState    // 方法,替換掉當前記錄在history中的路由資訊
}

// 訂閱history變更事件
window.onpopstate = function (event) {
    ...
}

從對比中可以看出,兩者的關係是非常密切的,history庫可以說是history物件的超集,是功能更強大的history物件。

簡單前端路由的實現

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>router</title>
</head>
<body>
    <ul> 
        <li><a href="#/">turn white</a></li> 
        <li><a href="#/blue">turn blue</a></li> 
        <li><a href="#/green">turn green</a></li> 
    </ul> 
<script>
    function Router() {
        this.routes = {};
        this.currentUrl = '';
    }
    <!--
    //針對不同的地址進行回撥的匹配
    //1:使用者在呼叫Router.route('address',function),在this.routes物件中進行記錄或者說address與function的匹配
    -->
    Router.prototype.route = function(path, callback) {
        this.routes[path] = callback || function(){};
    };
    <!--
    //處理hash的變化,針對不同的值,進行頁面的處理
    //1:在init中註冊過事件,在頁面load的時候,進行頁面的處理
    //2:在hashchange變化時,進行頁面的處理
    -->
    Router.prototype.refresh = function() {
        this.currentUrl = location.hash.slice(1) || '/';
        this.routes[this.currentUrl]();
    };
    <!--
    //1:在Router的prototype中定義init
    //2:在頁面load/hashchange事件觸發時,進行回撥處理
    -->
    Router.prototype.init = function() {
        window.addEventListener('load', this.refresh.bind(this));
        window.addEventListener('hashchange', this.refresh.bind(this));
    }
    window.Router = new Router();//在window物件中構建一個Router物件
    window.Router.init();//頁面初始化處理
    var content = document.querySelector('body');
    // change Page anything
    function changeBgColor(color) {
        content.style.backgroundColor = color;
    }
    Router.route('/', function() {
        changeBgColor('white');
    });
    Router.route('/blue', function() {
        changeBgColor('blue');
    });
    Router.route('/green', function() {
        changeBgColor('green');
    });
</script>
</body>
</html>

React-Router簡單實現

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>包裝方式</title>
</head>
<body>
<script>
	var body = document.querySelector('body'),
		newNode = null,
        append = function(str){
    		newNode = document.createElement("p");
    		newNode.innerHTML = str;
    		body.appendChild(newNode);
    	};
	
    // history庫
    var historyModule = {
        listener: [],
        listen: function (listener) {
            this.listener.push(listener);
            append('historyModule listen.')
        },
        updateLocation: function(){
            append('historyModule updateLocation tirgger.');
            this.listener.forEach(function(listener){
                listener('new localtion');
            })
        }
    }
    // Router 將使用 historyModule 物件,並對其包裝
    var Router = {
        source: {},
        //複製historyModule到Router中
        init: function(source){
            this.source = source;
        },
        //處理監聽事件,在Router對頁面進行處理時,利用historyModule中處理頁面
        listen: function(listener) {
            append('Router listen.');
            // 對 historyModule的listen進行了一層包裝
            return this.source.listen(function(location){
                append('Router listen tirgger.');
                listener(location);
            })
        }
    }
    // 將 historyModule 注入進 Router 中
    Router.init(historyModule);
    // Router 註冊監聽
    Router.listen(function(location){
        append(location + '-> Router setState.');
    })
    // historyModule 觸發監聽回撥(對頁面進行渲染等處理)
    historyModule.updateLocation();
</script>
</body>
</html>

其實上訴的操作就是隻是針對前端簡單路由+historyModule的升級處理。

react-router分析

BrowserRouter

用BrowserRouter 元件包裹整個App系統後,就是通過html5的history來實現無重新整理條件下的前端路由。
與BrowserRouter對應的是HashRouter,HashRouter使用url中的hash屬性來保證不重新重新整理的情況下同時渲染頁面。

import warning from "warning";
import React from "react";
import PropTypes from "prop-types";
import { createBrowserHistory as createHistory } from "history";//這裡的history就是上面第二個例子中的historyModule
import Router from "./Router"; //對應第二個例子中的Router物件

/**
 * The public API for a <Router> that uses HTML5 history. //這裡是重點
 */
class BrowserRouter extends React.Component {
  history = createHistory(this.props);
  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

export default BrowserRouter;

listen函式的註冊

React-Router/Router.js

/**
 * The public API for putting history on context. //這裡的道理類似於例子二中第二步
 */
class Router extends React.Component {
  
  static childContextTypes = {
    router: PropTypes.object.isRequired
  };

  getChildContext() {
    return {
      router: {
        ...this.context.router,
        history: this.props.history,
        route: {
          location: this.props.history.location,
          match: this.state.match
        }
      }
    };
  }

  state = {
    match: this.computeMatch(this.props.history.location.pathname)
  };

  computeMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  }

  componentWillMount() {
    const { children, history } = this.props;
    // Do this here so we can setState when a <Redirect> changes the
    // location in componentWillMount. This happens e.g. when doing
    // server rendering using a <StaticRouter>.
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }

  componentWillReceiveProps(nextProps) {
    warning(
      this.props.history === nextProps.history,
      "You cannot change <Router history>"
    );
  }

  componentWillUnmount() {
    this.unlisten();
  }

  render() {
    const { children } = this.props;
    return children ? React.Children.only(children) : null;
  }
}

export default Router;

Redirect.js

react-router/Redirect.js

//這裡省去其他庫的引用
import generatePath from "./generatePath";
/**
 * The public API for updating the location programmatically
 * with a component.
 */
class Redirect extends React.Component {
//這裡是從Context中拿到history等資料
  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,
        replace: PropTypes.func.isRequired
      }).isRequired,
      staticContext: PropTypes.object
    }).isRequired
  };

  isStatic() {
    return this.context.router && this.context.router.staticContext;
  }

  componentWillMount() {
    invariant(
      this.context.router,
      "You should not use <Redirect> outside a <Router>"
    );

    if (this.isStatic()) this.perform();
  }

  componentDidMount() {
    if (!this.isStatic()) this.perform();
  }

  componentDidUpdate(prevProps) {
    const prevTo = createLocation(prevProps.to);
    const nextTo = createLocation(this.props.to);

    if (locationsAreEqual(prevTo, nextTo)) {
      warning(
        false,
        `You tried to redirect to the same route you're currently on: ` +
          `"$