面試官: 你瞭解前端路由嗎?

面試官系列(3): 前端路由的實現


往期


文章目錄

  1. 基於hash的前端路由實現
  2. 基於hash的前端路由升級
  3. 基於H5 History的前端路由實現

前言

前端路由是現代SPA應用必備的功能,每一個現代前端框架都有對應的實現,例如vue-router、react-router。javascript

咱們不想探究vue-router或者react-router們的實現,由於不論是哪一種路由無外乎用兼容性更好的hash實現或者是H5 History實現,與框架幾個只須要作相應的封裝便可。html

提早聲明: 咱們沒有對傳入的參數進行及時判斷而規避錯誤,也沒有考慮兼容性問題,僅僅對核心方法進行了實現.前端


1.hash路由

hash路由一個明顯的標誌是帶有#,咱們主要是經過監聽url中的hash變化來進行路由跳轉。vue

hash的優點就是兼容性更好,在老版IE中都有運行,問題在於url中一直存在#不夠美觀,並且hash路由更像是Hack而非標準,相信隨着發展更加標準化的History API會逐步蠶食掉hash路由的市場。 java

1.1 初始化class

咱們用Class關鍵字初始化一個路由.react

class Routers {
  constructor() {
    // 以鍵值對的形式儲存路由
    this.routes = {};
    // 當前路由的URL
    this.currentUrl = '';
  }
}
複製代碼

1.2 實現路由hash儲存與執行

在初始化完畢後咱們須要思考兩個問題:git

  1. 將路由的hash以及對應的callback函數儲存
  2. 觸發路由hash變化後,執行對應的callback函數
class Routers {
  constructor() {
    this.routes = {};
    this.currentUrl = '';
  }
  // 將path路徑與對應的callback函數儲存
  route(path, callback) {
    this.routes[path] = callback || function() {};
  }
  // 刷新
  refresh() {
    // 獲取當前URL中的hash路徑
    this.currentUrl = location.hash.slice(1) || '/';
    // 執行當前hash路徑的callback函數
    this.routes[this.currentUrl]();
  }
}
複製代碼

1.3 監聽對應事件

那麼咱們只須要在實例化Class的時候監聽上面的事件便可.github

class Routers {
  constructor() {
    this.routes = {};
    this.currentUrl = '';
    this.refresh = this.refresh.bind(this);
    window.addEventListener('load', this.refresh, false);
    window.addEventListener('hashchange', this.refresh, false);
  }

  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    this.routes[this.currentUrl]();
  }
}
複製代碼

對應效果以下: 面試

完整示例vue-router

點擊這裏 hash router by 尋找海藍 (@xiaomuzhu) on CodePen.


2.增長回退功能

上一節咱們只實現了簡單的路由功能,沒有咱們經常使用的回退前進功能,因此咱們須要進行改造。

2.1 實現後退功能

咱們在須要建立一個數組history來儲存過往的hash路由例如/blue,而且建立一個指針currentIndex來隨着後退前進功能移動來指向不一樣的hash路由。

class Routers {
  constructor() {
    // 儲存hash與callback鍵值對
    this.routes = {};
    // 當前hash
    this.currentUrl = '';
    // 記錄出現過的hash
    this.history = [];
    // 做爲指針,默認指向this.history的末尾,根據後退前進指向history中不一樣的hash
    this.currentIndex = this.history.length - 1;
    this.refresh = this.refresh.bind(this);
    this.backOff = this.backOff.bind(this);
    window.addEventListener('load', this.refresh, false);
    window.addEventListener('hashchange', this.refresh, false);
  }

  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    // 將當前hash路由推入數組儲存
    this.history.push(this.currentUrl);
    // 指針向前移動
    this.currentIndex++;
    this.routes[this.currentUrl]();
  }
  // 後退功能
  backOff() {
    // 若是指針小於0的話就不存在對應hash路由了,所以鎖定指針爲0便可
    this.currentIndex <= 0
      ? (this.currentIndex = 0)
      : (this.currentIndex = this.currentIndex - 1);
    // 隨着後退,location.hash也應該隨之變化
    location.hash = `#${this.history[this.currentIndex]}`;
    // 執行指針目前指向hash路由對應的callback
    this.routes[this.history[this.currentIndex]]();
  }
}
複製代碼

咱們看起來實現的不錯,但是出現了Bug,在後退的時候咱們每每須要點擊兩下。

點擊查看Bug示例 hash router by 尋找海藍 (@xiaomuzhu) on CodePen.

問題在於,咱們每次在後退都會執行相應的callback,這會觸發refresh()執行,所以每次咱們後退,history中都會被push新的路由hash,currentIndex也會向前移動,這顯然不是咱們想要的。

refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    // 將當前hash路由推入數組儲存
    this.history.push(this.currentUrl);
    // 指針向前移動
    this.currentIndex++;
    this.routes[this.currentUrl]();
  }
複製代碼

如圖所示,咱們每次點擊後退,對應的指針位置和數組被打印出來

2.2 完整實現hash Router

咱們必須作一個判斷,若是是後退的話,咱們只須要執行回調函數,不須要添加數組和移動指針。

class Routers {
  constructor() {
    // 儲存hash與callback鍵值對
    this.routes = {};
    // 當前hash
    this.currentUrl = '';
    // 記錄出現過的hash
    this.history = [];
    // 做爲指針,默認指向this.history的末尾,根據後退前進指向history中不一樣的hash
    this.currentIndex = this.history.length - 1;
    this.refresh = this.refresh.bind(this);
    this.backOff = this.backOff.bind(this);
    // 默認不是後退操做
    this.isBack = false;
    window.addEventListener('load', this.refresh, false);
    window.addEventListener('hashchange', this.refresh, false);
  }

  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    if (!this.isBack) {
      // 若是不是後退操做,且當前指針小於數組總長度,直接截取指針以前的部分儲存下來
      // 此操做來避免當點擊後退按鈕以後,再進行正常跳轉,指針會停留在原地,而數組添加新hash路由
      // 避免再次形成指針的不匹配,咱們直接截取指針以前的數組
      // 此操做同時與瀏覽器自帶後退功能的行爲保持一致
      if (this.currentIndex < this.history.length - 1)
        this.history = this.history.slice(0, this.currentIndex + 1);
      this.history.push(this.currentUrl);
      this.currentIndex++;
    }
    this.routes[this.currentUrl]();
    console.log('指針:', this.currentIndex, 'history:', this.history);
    this.isBack = false;
  }
  // 後退功能
  backOff() {
    // 後退操做設置爲true
    this.isBack = true;
    this.currentIndex <= 0
      ? (this.currentIndex = 0)
      : (this.currentIndex = this.currentIndex - 1);
    location.hash = `#${this.history[this.currentIndex]}`;
    this.routes[this.history[this.currentIndex]]();
  }
}
複製代碼

查看完整示例 Hash Router by 尋找海藍 (@xiaomuzhu) on CodePen.

前進的部分就不實現了,思路咱們已經講得比較清楚了,能夠看出來,hash路由這種方式確實有點繁瑣,因此HTML5標準提供了History API供咱們使用。


3. HTML5新路由方案

3.1 History API

咱們能夠直接在瀏覽器中查詢出History API的方法和屬性。

固然,咱們經常使用的方法實際上是有限的,若是想全面瞭解能夠去MDN查詢History API的資料

咱們只簡單看一下經常使用的API

window.history.back();       // 後退
window.history.forward();    // 前進
window.history.go(-3);       // 後退三個頁面

複製代碼

history.pushState用於在瀏覽歷史中添加歷史記錄,可是並不觸發跳轉,此方法接受三個參數,依次爲:

state:一個與指定網址相關的狀態對象,popstate事件觸發時,該對象會傳入回調函數。若是不須要這個對象,此處能夠填null
title:新頁面的標題,可是全部瀏覽器目前都忽略這個值,所以這裏能夠填null
url:新的網址,必須與當前頁面處在同一個域。瀏覽器的地址欄將顯示這個網址。

history.replaceState方法的參數與pushState方法如出一轍,區別是它修改瀏覽歷史中當前紀錄,而非添加記錄,一樣不觸發跳轉。

popstate事件,每當同一個文檔的瀏覽歷史(即history對象)出現變化時,就會觸發popstate事件。

須要注意的是,僅僅調用pushState方法或replaceState方法 ,並不會觸發該事件,只有用戶點擊瀏覽器倒退按鈕和前進按鈕,或者使用 JavaScript 調用backforwardgo方法時纔會觸發。

另外,該事件只針對同一個文檔,若是瀏覽歷史的切換,致使加載不一樣的文檔,該事件也不會觸發。

以上API介紹選自history對象,能夠點擊查看完整版,咱們不想佔用過多篇幅來介紹API。

3.2 新標準下路由的實現

上一節咱們介紹了新標準的History API,相比於咱們在Hash 路由實現的那些操做,很顯然新標準讓咱們的實現更加方便和可讀。

因此一個mini路由實現起來其實很簡單

class Routers {
  constructor() {
    this.routes = {};
    // 在初始化時監聽popstate事件
    this._bindPopState();
  }
  // 初始化路由
  init(path) {
    history.replaceState({path: path}, null, path);
    this.routes[path] && this.routes[path]();
  }
  // 將路徑和對應回調函數加入hashMap儲存
  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  // 觸發路由對應回調
  go(path) {
    history.pushState({path: path}, null, path);
    this.routes[path] && this.routes[path]();
  }
  // 監聽popstate事件
  _bindPopState() {
    window.addEventListener('popstate', e => {
      const path = e.state && e.state.path;
      this.routes[path] && this.routes[path]();
    });
  }
}
複製代碼

點擊查看H5路由 H5 Router by 尋找海藍 (@xiaomuzhu) on CodePen.


小結

咱們大體探究了前端路由的兩種實現方法,在沒有兼容性要求的狀況下顯然符合標準的History API實現的路由是更好的選擇。

想更深刻了解前端路由實現能夠閱讀vue-router代碼,除去開發模式代碼、註釋和類型檢測代碼,核心代碼並很少,適合閱讀。


下期預告

下期準備一篇關於雙向綁定的話題,由於許多人只知道Object.definedProperty,禁不住深究:

  1. 同是數據劫持,與Proxy相比有何優劣?
  2. 除了數據劫持能夠實現雙向綁定還有沒有其餘方法?
  3. 其餘方法(例如髒檢測、Observable模式、數據模型等)與數據劫持相比優劣如何?

因爲涉及的框架和知識點過多,我開了一個頭已經小2000字了,,在考慮要不要分上下篇發出來,不過我相信它解決你對雙向綁定全部的疑問。

相關文章
相關標籤/搜索