前端路由跳轉基本原理

舞動乾坤發表於2019-02-01

前端路由跳轉基本原理

目前前端三傑 Angular、React、Vue 都推介單頁面應用 SPA 開發模式,在路由切換時替換 DOM Tree 中最小修改的部分 DOM,來減少原先因為多頁應用的頁面跳轉帶來的巨量效能損耗。它們都有自己的典型路由解決方案,@angular/router、react-router、vue-router。

一般來說,這些路由外掛總是提供兩種不同方式的路由方式: Hash 和 History,有時也會提供非瀏覽器環境下的路由方式 Abstract,在 vue-router 中是使用了外觀模式將幾種不同的路由方式提供了一個一致的高層介面,讓我們可以更解耦的在不同路由方式中切換。

值得一提的是,Hash 和 History 除了外觀上的不同之外,還一個區別是:Hash 方式的狀態儲存需要另行傳遞,而 HTML5 History 原生提供了自定義狀態傳遞的能力,我們可以直接利用其來傳遞資訊。

下面我們具體看看這兩種方式都有哪些特點,並提供簡單的實現,更復雜的功能比如懶載入、動態路徑匹配、巢狀路由、路由別名等等,可以關注一下後面的 vue-router 原始碼解讀方面的部落格。

1. Hash

1.1 相關 Api

Hash 方法是在路由中帶有一個 #,主要原理是通過監聽 # 後的 URL 路徑識別符號的更改而觸發的瀏覽器 hashchange 事件,然後通過獲取 location.hash 得到當前的路徑識別符號,再進行一些路由跳轉的操作,參見 MDN

  1. location.href:返回完整的 URL
  2. location.hash:返回 URL 的錨部分
  3. location.pathname:返回 URL 路徑名
  4. hashchange 事件:當 location.hash 發生改變時,將觸發這個事件

比如訪問一個路徑 http://sherlocked93.club/base/#/page1,那麼上面幾個值分別為:

# http://sherlocked93.club/base/#/page1
{
  "href": "http://sherlocked93.club/base/#/page1",
  "pathname": "/base/",
  "hash": "#/page1"
}
複製程式碼複製程式碼

注意: Hash 方法是利用了相當於頁面錨點的功能,所以與原來的通過錨點定位來進行頁面滾動定位的方式衝突,導致定位到錯誤的路由路徑,因此需要採用別的辦法,之前在寫 progress-catalog 這個外掛碰到了這個情況。

1.2 例項

這裡簡單做一個實現,原理是把目標路由和對應的回撥記錄下來,點選跳轉觸發 hashchange 的時候獲取當前路徑並執行對應回撥,效果:

前端路由跳轉基本原理

class RouterClass {
  constructor() {
    this.routes = {}        // 記錄路徑識別符號對應的cb
    this.currentUrl = ''    // 記錄hash只為方便執行cb
    window.addEventListener('load', () => this.render())
    window.addEventListener('hashchange', () => this.render())
  }

/* 初始化 */ static init() { window.Router = new RouterClass() }

/* 註冊路由和回撥 */ route(path, cb) { this.routes[path] = cb || function() {} }

複製程式碼

/* 記錄當前hash,執行cb */ render() { this.currentUrl = location.hash.slice(1) || '/' this.routesthis.currentUrl } } 複製程式碼複製程式碼

具體實現參照 CodePen

如果希望使用指令碼來控制 Hash 路由的後退,可以將經歷的路由記錄下來,路由後退跳轉的實現是對 location.hash 進行賦值。但是這樣會引發重新引發 hashchange 事件,第二次進入 render 。所以我們需要增加一個標誌位,來標明進入 render 方法是因為回退進入的還是使用者跳轉

前端路由跳轉基本原理

class RouterClass {
  constructor() {
    this.isBack = false
    this.routes = {}        // 記錄路徑識別符號對應的cb
    this.currentUrl = ''    // 記錄hash只為方便執行cb
    this.historyStack = []  // hash棧
    window.addEventListener('load', () => this.render())
    window.addEventListener('hashchange', () => this.render())
  }

/* 初始化 */ static init() { window.Router = new RouterClass() }

/* 記錄path對應cb */ route(path, cb) { this.routes[path] = cb || function() {} }

/* 入棧當前hash,執行cb */ render() { if (this.isBack) { // 如果是由backoff進入,則置false之後return this.isBack = false // 其他操作在backoff方法中已經做了 return } this.currentUrl = location.hash.slice(1) || '/' this.historyStack.push(this.currentUrl) this.routesthis.currentUrl }

複製程式碼

/* 路由後退 */ back() { this.isBack = true this.historyStack.pop() // 移除當前hash,回退到上一個 const { length } = this.historyStack if (!length) return let prev = this.historyStack[length - 1] // 拿到要回退到的目標hash location.hash = #<span class="hljs-subst">${ prev }</span>複製程式碼 this.currentUrl = prev this.routesprev // 執行對應cb } } 複製程式碼複製程式碼

程式碼實現參考 CodePen

2. HTML5 History Api

2.1 相關 Api

HTML5 提供了一些路由操作的 Api,關於使用可以參看 這篇 MDN 上的文章,這裡就列舉一下常用 Api 和他們的作用,具體引數什麼的就不介紹了,MDN 上都有

  1. history.go(n):路由跳轉,比如n為 2 是往前移動2個頁面,n為 -2 是向後移動2個頁面,n為0是重新整理頁面
  2. history.back():路由後退,相當於 history.go(-1)
  3. history.forward():路由前進,相當於 history.go(1)
  4. history.pushState():新增一條路由歷史記錄,如果設定跨域網址則報錯
  5. history.replaceState():替換當前頁在路由歷史記錄的資訊
  6. popstate 事件:當活動的歷史記錄發生變化,就會觸發 popstate 事件,在點選瀏覽器的前進後退按鈕或者呼叫上面前三個方法的時候也會觸發,參見 MDN

2.2 例項

將之前的例子改造一下,在需要路由跳轉的地方使用 history.pushState 來入棧並記錄 cb,前進後退的時候監聽 popstate 事件拿到之前傳給 pushState 的引數並執行對應 cb,因為借用了瀏覽器自己的 Api,因此程式碼看起來整潔不少

前端路由跳轉基本原理

class RouterClass {
  constructor(path) {
    this.routes = {}        // 記錄路徑識別符號對應的cb
    history.replaceState({ path }, null, path)	// 進入狀態
    this.routes[path] && this.routes[path]()
    window.addEventListener('popstate', e => {
      const path = e.state && e.state.path
      this.routes[path] && this.routes[path]()
    })
  }

/* 初始化 */ static init() { window.Router = new RouterClass(location.pathname) }

/* 註冊路由和回撥 */ route(path, cb) { this.routes[path] = cb || function() {} }

複製程式碼

/* 跳轉路由,並觸發路由對應回撥 */ go(path) { history.pushState({ path }, null, path) this.routes[path] && this.routespath } } 複製程式碼複製程式碼

Hash 模式是使用 URL 的 Hash 來模擬一個完整的 URL,因此當 URL 改變的時候頁面並不會過載。History 模式則會直接改變 URL,所以在路由跳轉的時候會丟失一些地址資訊,在重新整理或直接訪問路由地址的時候會匹配不到靜態資源。因此需要在伺服器上配置一些資訊,讓伺服器增加一個覆蓋所有情況的候選資源,比如跳轉 index.html 什麼的,一般來說是你的 app 依賴的頁面,事實上 vue-router 等庫也是這麼推介的,還提供了常見的伺服器配置

程式碼實現參考 CodePen


網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~

參考:

  1. history | MDN
  2. hashchange | MDN
  3. Manipulating the browser history | MDN
  4. 前端路由的基本原理 - 大史不說話
  5. History 物件 -- JavaScript 標準參考教程

最近折騰了一個技術公眾號 前端下午茶 ,可以去搜一下,希望對大家的前端之旅有所幫助呀~

相關文章