天天看點

實作一個前端路由,如何實作浏覽器的前進與後退 ?4. 最後

實作一個前端路由,如何實作浏覽器的前進與後退 ?4. 最後

1. 需求

如果要你實作一個前端路由,應該如何實作浏覽器的前進與後退 ?

2. 問題

首先浏覽器中主要有這幾個限制,讓前端不能随意的操作浏覽器的浏覽紀錄:

  • 沒有提供監聽前進後退的事件。
  • 不允許開發者讀取浏覽紀錄,也就是 js 讀取不了浏覽紀錄。
  • 使用者可以手動輸入位址,或使用浏覽器提供的前進後退來改變 url。

是以要實作一個自定義路由,解決方案是自己維護一份路由曆史的記錄,進而區分 前進、重新整理、回退。

下面介紹具體的方法。

3. 方法

目前筆者知道的方法有兩種,一種是 在數組後面進行增加與删除,另外一種是 利用棧的後進先出原理。

3.1 在數組最後進行 增加與删除

通過監聽路由的變化事件 hashchange,與路由的第一次加載事件 load ,判斷如下情況:

  • url 存在于浏覽記錄中即為後退,後退時,把目前路由後面的浏覽記錄删除。
  • url 不存在于浏覽記錄中即為前進,前進時,往數組裡面 push 目前的路由。
  • url 在浏覽記錄的末端即為重新整理,重新整理時,不對路由數組做任何操作。

另外,應用的路由路徑中可能允許相同的路由出現多次(例如 A -> B -> A),是以給每個路由添加一個 key 值來區分相同路由的不同執行個體。

注意:這個浏覽記錄需要存儲在 sessionStorage 中,這樣使用者重新整理後浏覽記錄也可以恢複。

筆者之前實作的 用原生 js 實作的輕量級路由 ,就是用這種方法實作的,具體代碼如下:

// 路由構造函數
function Router() {
        this.routes = {}; //儲存注冊的所有路由
        this.routerViewId = "#routerView"; // 路由挂載點 
        this.stackPages = true; // 多級頁面緩存
        this.history = []; // 路由曆史
}

Router.prototype = {
        init: function(config) {
            var self = this;
            //頁面首次加載 比對路由
            window.addEventListener('load', function(event) {
                // console.log('load', event);
                self.historyChange(event)
            }, false)

            //路由切換
            window.addEventListener('hashchange', function(event) {
                // console.log('hashchange', event);
                self.historyChange(event)
            }, false)

        },
        // 路由曆史紀錄變化
        historyChange: function(event) {
            var currentHash = util.getParamsUrl();
            var nameStr = "router-history"
            this.history = window.sessionStorage[nameStr] ? JSON.parse(window.sessionStorage[nameStr]) : []

            var back = false, // 後退
                refresh = false, // 重新整理
                forward = false, // 前進
                index = 0,
                len = this.history.length;

            // 比較目前路由的狀态,得出是後退、前進、重新整理的狀态。
            for (var i = 0; i < len; i++) {
                var h = this.history[i];
                if (h.hash === currentHash.path && h.key === currentHash.query.key) {
                    index = i
                    if (i === len - 1) {
                        refresh = true
                    } else {
                        back = true
                    }
                    break;
                } else {
                    forward = true
                }
            }
            if (back) {
                 // 後退,把曆史紀錄的最後一項删除
                this.historyFlag = 'back'
                this.history.length = index + 1
            } else if (refresh) {
                 // 重新整理,不做其他操作
                this.historyFlag = 'refresh'
            } else {
                // 前進,添加一條曆史紀錄
                this.historyFlag = 'forward'
                var item = {
                    key: currentHash.query.key,
                    hash: currentHash.path,
                    query: currentHash.query
                }
                this.history.push(item)
            }
            // 如果不需要頁面緩存功能,每次都是重新整理操作
            if (!this.stackPages) {
                this.historyFlag = 'forward'
            }
            window.sessionStorage[nameStr] = JSON.stringify(this.history)
        },
    }
           

以上代碼隻列出本次文章相關的内容,完整的内容請看 原生 js 實作的輕量級路由,且頁面跳轉間有緩存功能。

3.2 利用棧的 後進者先出,先進者後出 原理

在說第二個方法之前,先來弄明白棧的定義與後進者先出,先進者後出原理。

3.2.1 定義

棧的特點:後進者先出,先進者後出。

舉一個生活中的例子說明:就是一摞疊在一起的盤子。我們平時放盤子的時候,都是從下往上一個一個放;取的時候,我們也是從上往下一個一個地依次取,不能從中間任意抽出。

實作一個前端路由,如何實作浏覽器的前進與後退 ?4. 最後

因為棧的後進者先出,先進者後出的特點,是以隻能棧一端進行插入和删除操作。這也和第一個方法的原理有異曲同工之妙。

下面用 JavaScript 來實作一個順序棧:

// 基于數組實作的順序棧
class ArrayStack {
  constructor(n) {
      this.items = [];  // 數組
      this.count = 0;   // 棧中元素個數
      this.n = n;       // 棧的大小
  }

  // 入棧操作
  push(item) {
    // 數組空間不夠了,直接傳回 false,入棧失敗。
    if (this.count === this.n) return false;
    // 将 item 放到下标為 count 的位置,并且 count 加一
    this.items[this.count] = item;
    ++this.count;
    return true;
  }
  
  // 出棧操作
  pop() {
    // 棧為空,則直接傳回 null
    if (this.count == 0) return null;
    // 傳回下标為 count-1 的數組元素,并且棧中元素個數 count 減一
    let tmp = items[this.count-1];
    --this.count;
    return tmp;
  }
}
           

其實 JavaScript 中,數組是自動擴容的,并不需要指定數組的大小,也就是棧的大小 n 可以不指定的。

3.2.2 應用

棧的經典應用: 函數調用棧

作業系統給每個線程配置設定了一塊獨立的記憶體空間,這塊記憶體被組織成“棧”這種結構, 用來存儲函數調用時的臨時變量。每進入一個函數,就會将臨時變量作為一個棧幀入棧,當被調用函數執行完成,傳回之後,将這個函數對應的棧幀出棧。為了讓你更好地了解,我們一塊來看下這段代碼的執行過程。

function add(x, y) {
   let sum = 0;
   sum = x + y;
   return sum;
}

function main() {
   let a = 1; 
   let ret = 0;
   let res = 0;
   ret = add(3, 5);
   res = a + ret;
   console.log("res: ", res);
   reuturn 0;
}

main();
           

上面代碼也很簡單,就是執行 main 函數求和,main 函數裡面又調用了 add 函數,先調用的先進入棧。

執行過程如下:

實作一個前端路由,如何實作浏覽器的前進與後退 ?4. 最後

3.2.3 實作浏覽器的前進、後退

第二個方法就是:用兩個棧實作浏覽器的前進、後退功能。

我們使用兩個棧,X 和 Y,我們把首次浏覽的頁面依次壓入棧 X,當點選後退按鈕時,再依次從棧 X 中出棧,并将出棧的資料依次放入棧 Y。當我們點選前進按鈕時,我們依次從棧 Y 中取出資料,放入棧 X 中。當棧 X 中沒有資料時,那就說明沒有頁面可以繼續後退浏覽了。當棧 Y 中沒有資料,那就說明沒有頁面可以點選前進按鈕浏覽了。

比如你順序檢視了 a,b,c 三個頁面,我們就依次把 a,b,c 壓入棧,這個時候,兩個棧的資料如下:

實作一個前端路由,如何實作浏覽器的前進與後退 ?4. 最後

當你通過浏覽器的後退按鈕,從頁面 c 後退到頁面 a 之後,我們就依次把 c 和 b 從棧 X 中彈出,并且依次放入到棧 Y。這個時候,兩個棧的資料就是這個樣子:

實作一個前端路由,如何實作浏覽器的前進與後退 ?4. 最後

這個時候你又想看頁面 b,于是你又點選前進按鈕回到 b 頁面,我們就把 b 再從棧 Y 中出棧,放入棧 X 中。此時兩個棧的資料是這個樣子:

實作一個前端路由,如何實作浏覽器的前進與後退 ?4. 最後

這個時候,你通過頁面 b 又跳轉到新的頁面 d 了,頁面 c 就無法再通過前進、後退按鈕重複檢視了,是以需要清空棧 Y。此時兩個棧的資料這個樣子:

實作一個前端路由,如何實作浏覽器的前進與後退 ?4. 最後

如果用代碼來實作,會是怎樣的呢 ?各位可以想一下。

其實就是在第一個方法的代碼裡面, 添加多一份路由曆史紀錄的數組即可,對這兩份曆史紀錄的操作如上面示例圖所示即可,也就是對數組的增加和删除操作而已, 這裡就不展開了。

其中第二個方法與參考了 王争老師的 資料結構與算法之美。

4. 最後

部落格首更位址 :https://github.com/biaochenxuying/blog

往期精文

  1. 一張思維導圖輔助你深入了解 Vue | Vue-Router | Vuex 源碼架構
  2. Vue + TypeScript + Element 項目實戰及踩坑記
  3. vue-cli3.x 新特性及踩坑記
  4. 那些必會用到的 ES6 精粹

參考文章:資料結構與算法之美

歡迎關注以下公衆号 全棧修煉,學到不一樣的武功秘籍 !

關注公衆号并回複 福利 可領取免費學習資料,福利詳情請猛戳:

繼續閱讀