前端路由跳轉基本原理

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

通常來講,這些路由插件老是提供兩種不一樣方式的路由方式: Hash 和 History,有時也會提供非瀏覽器環境下的路由方式 Abstract,在 vue-router 中是使用了外觀模式將幾種不一樣的路由方式提供了一個一致的高層接口,讓咱們能夠更解耦的在不一樣路由方式中切換。html

值得一提的是,Hash 和 History 除了外觀上的不一樣以外,還一個區別是:Hash 方式的狀態保存須要另行傳遞,而 HTML5 History 原生提供了自定義狀態傳遞的能力,咱們能夠直接利用其來傳遞信息。前端

下面咱們具體看看這兩種方式都有哪些特色,並提供簡單的實現,更復雜的功能好比懶加載、動態路徑匹配、嵌套路由、路由別名等等,能夠關注一下後面的 vue-router 源碼解讀方面的博客。vue

感興趣的同窗能夠加文末的微信羣,一塊兒討論吧~html5

1. Hash

1.1 相關 Api

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

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

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

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

注意: Hash 方法是利用了至關於頁面錨點的功能,因此與原來的經過錨點定位來進行頁面滾動定位的方式衝突,致使定位到錯誤的路由路徑,所以須要採用別的辦法,以前在寫 progress-catalog 這個插件碰到了這個狀況。git

1.2 實例

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

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.routes[this.currentUrl]()
  }
}
複製代碼

具體實現參照 CodePenvue-router

若是但願使用腳原本控制 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.routes[this.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 = `#${ prev }`
    this.currentUrl = prev
    this.routes[prev]()                       // 執行對應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.routes[path]()
  }
}
複製代碼

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 標準參考教程

PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~

另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~

相關文章
相關標籤/搜索