SPA路由機制詳解(看不懂不要錢~~)



前言

總所周知,隨着前端應用的業務功能起來越複雜,用戶對於使用體驗的要求愈來愈高,單面(SPA)成爲前端應用的主流形式。而大型單頁應用最顯著特色之一就是採用的前端路由跳轉子頁面系統,經過改變頁面的URL,在不從新請求頁面的狀況下,更新頁面視圖。javascript

更新視圖可是瀏覽器不從新渲染整個頁面,只是從新渲染部分子頁面,加載速度快,頁面反應靈活,這是 SPA 的優點,這也是前端路由原理的核心,這會給人一種彷彿在操做 APP 同樣的感受,目前在瀏覽器環境中實現這一功能的方式主要有兩種:css

  • 利用 URLhash(#)
  • 利用 H5 新增方法 History interface

利用URLHash(#)

H5 尚未流行開來時,通常 SPA 都採用 urlhash(#) 做爲錨點,獲取到 # 以後的值,並監聽其改變,再進行渲染對應的子頁面。網易雲音樂官網就是利用的此技術。html

例如,你的地址爲http://localhost:8888/#/abc 那麼利用 location.hash 輸出的內容就爲 #/abc前端

那麼我就先從 location 這個對象提及。vue

先來看看location的官方屬性有哪些java

屬性 描述
hash 設置或返回從 # 開始的 URL (錨)
host 設置或返回主機名和當前 URL 的端口號
hostname 設置或返回當前 URL 的主機名
href 設置或返回完整的 URL
pathname 設置或返回當前 URL 的路徑部分
port 設置或返回當前 URL 的端口號
protocol 設置或返回當前 URL 的協議
search 設置或返回從 ? 開始的 URL 部分

由上表格能夠知道,咱們能夠輕易的獲取到 # 以後的部分,那麼拿到這個部分咱們怎麼監聽其變化以及對應的子頁面進行改變呢?jquery

window 對象中有一個事件是專門監聽hash的變化,那就是onhashchange,首先咱們須要監聽此事件:git

<body>
  <h1 id="id"></h1>
  <a href="#/id1">id1</a>
  <a href="#/id2">id2</a>
  <a href="#/id3">id3</a>
</body>

<script> window.addEventListener('hashchange', e => { e.preventDefault() document.querySelector('#id').innerHTML = location.hash }) </script>
複製代碼

img

可見此時咱們已經徹底監聽到了 URL 的變化,頁面上的內容也對應改變了。 那麼,該如何載入不一樣的頁面呢,目前來講有三種方式:github

  • 尋找節點內容並改變(也就是上面咱們演示的內容)
  • import 一個 JS 文件,文件內部 export 模版字符串
  • 利用 AJAX 加載對應的 HTML 模版

第一種方式已經演示過,不過這種方式侷限性太大,下面我會演示另外兩種方式加載頁面。ajax

import 方式

定義一個 JS 文件,名爲 demo1.js,在裏面輸入內容:

const str = ` <div> 我是import進來的JS文件 </div> `
export default str
複製代碼

在主文件裏 import 進來,並進行測試(使用 Chrome 必定要使用服務器開啓,或者直接用火狐打開):

<body>
  <h1 id="id"></h1>
  <a href="#/id1">id1</a>
  <a href="#/id2">id2</a>
  <a href="#/id3">id3</a>
</body>
<!-- 在 HTML 導入文件記得要加上 type="module" -->
<script type="module"> import demo1 from './demo1.js' document.querySelector('#id').innerHTML = demo1 window.addEventListener('hashchange', e => { e.preventDefault() document.querySelector('#id').innerHTML = location.hash }) </script>
複製代碼

img

可見導入文件已經生效,目前大部分框架編譯事後是採用相似此種方式處理。

例如,vue 框架,.vue 文件是一個自定義的文件類型,用類 HTML 語法描述一個 Vue 組件。每一個 .vue 文件包含三種類型的頂級語言塊 <template><script><style>vue-loader 會解析文件,提取每一個語言塊,若有必要會經過其它 loader 處理,最後將他們組裝成一個 CommonJS 模塊,module.exports 出一個 Vue.js 組件對象。。

AJAX 方式

本篇文章是詳解路由機制,AJAX 就直接採用 JQuery 這個輪子。

定義一個 HTML 文件,名爲 demo2.html,在裏面寫入一些內容(因爲主頁面已經有headbody等根標籤,此文件只需寫入須要替換的標籤):

<div>
  我是AJAX加載進來的HTML文件
</div>
複製代碼

咱們在主文件裏寫入,並進行測試:

<body>
  <h1 id="id"></h1>
  <a href="#/id1">id1</a>
  <a href="#/id2">id2</a>
  <a href="#/id3">id3</a>
</body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="module"> // import demo1 from './demo1.js' // document.querySelector('#id').innerHTML = demo1 $.ajax({ url: './demo2.html', success: (res) => { document.querySelector('#id').innerHTML = res } }) window.addEventListener('hashchange', e => { e.preventDefault() document.querySelector('#id').innerHTML = location.hash }) </script>
複製代碼

img

可見,利用 AJAX 加載進來的文件也已經生效。

既然加載不一樣頁面的內容都已經生效,那麼只須要包裝一下咱們的監聽,利用觀察者模式封裝路由的變化:

<body>
  <h1 id="id">我是空白頁</h1>
  <a href="#/id1">id1</a>
  <a href="#/id2">id2</a>
  <a href="#/id3">id3</a>
</body>
<script type="module"> import demo1 from './demo1.js' // 建立一個 newRouter 類 class newRouter { // 初始化路由信息 constructor() { this.routes = {}; this.currentUrl = ''; } // 傳入 URL 以及 根據 URL 對應的回調函數 route(path, callback = () => {}) { this.routes[path] = callback; } // 切割 hash,渲染頁面 refresh() { this.currentUrl = location.hash.slice(1) || '/'; this.routes[this.currentUrl] && this.routes[this.currentUrl](); } // 初始化 init() { window.addEventListener('load', this.refresh.bind(this), false); window.addEventListener('hashchange', this.refresh.bind(this), false); } } // new 一個 Router 實例 window.Router = new newRouter(); // 路由實例初始化 window.Router.init(); // 獲取關鍵節點 var content = document.querySelector('#id'); Router.route('/id1', () => { content.innerHTML = 'id1' }); Router.route('/id2', () => { content.innerHTML = demo1 }); Router.route('/id3', () => { $.ajax({ url: './demo2.html', success: (res) => { content.innerHTML = res } }) }); </script>
複製代碼

效果以下:

img

至此,利用 hash(#) 進行前端路由管理都已實現。

利用 H5 新增方法 History interface

上面使用的 hash 法實現路由當然不錯,可是問題就是實在太醜~ 若是在微信或者其餘不顯示 URLAPP 中使用,倒也無所謂,可是若是在通常的瀏覽器中使用就會遇到問題了。

由此,H5 的 History 模式,解決了這一問題。

H5 以前, History 僅僅只有一下幾個 API

API 說明
back() 回退到上次訪問的 URL (與瀏覽器點擊後退按鈕相同)
forward() 前進到回退以前的 URL (與瀏覽器點擊向前按鈕相同)
go(n) n 接收一個整數,移動到該整數指定的頁面,好比go(1)至關於forward()go(-1) 至關於 back()go(0)至關於刷新當前頁面

若是移動的位置超出了訪問歷史的邊界,以上三個方法並不報錯,而是靜默失敗。

然而,到了 H5 的時代,新的 H5 則賦予了其更多的新特性:

往返緩存

默認狀況下,瀏覽器會緩存當前會話頁面,這樣當下一個頁面點擊後退按鈕,或前一個頁面點擊前進按鈕,瀏覽器便會從緩存中提取並加載此頁面,這個特性被稱爲「往返緩存」。

PS: 此緩存會保留頁面數據、DOM和js狀態,其實是將整個頁面無缺完好地保留。

往歷史記錄棧中添加記錄:pushState(state, title, url)

瀏覽器支持度: IE10+

  • state: 一個 JS 對象(不大於640kB),主要用於在 popstate 事件中做爲參數被獲取。若是不須要這個對象,此處能夠填 null
  • title: 新頁面的標題,部分瀏覽器(好比 Firefox )忽略此參數,所以通常爲 null
  • url: 新歷史記錄的地址,可爲頁面地址,也可爲一個錨點值,新 url 必須與當前 url 處於同一個域,不然將拋出異常,此參數若沒有特別標註,會被設爲當前文檔 url

栗子:

// 如今是 localhost/1.html
const stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');

// 瀏覽器地址欄將當即變成 localhost/2.html
// 但!!!
// 不會跳轉到 2.html
// 不會檢查 2.html 是否存在
// 不會在 popstate 事件中獲取
// 不會觸發頁面刷新

// 這個方法僅僅是添加了一條最新記錄
複製代碼

除此以外,仍有幾點須要注意:

  • url 設爲錨點值時不會觸發 hashchange
  • 根據同源策略,若是設置不一樣域名地址,會報錯,這樣作的目的是:防止用戶覺得它們是同一個網站,若沒有此限制,將很容易進行 XSSCSRF 等攻擊方式

改變當前的歷史記錄:replaceState(state, title, url)

瀏覽器支持度: IE10+

  • 參數含義同 pushstate
  • 改變當前的歷史記錄而不是添加新的記錄
  • 一樣不會觸發 popstate

history.state

瀏覽器支持度: IE10+

  • 返回當前歷史記錄的 state

popstate

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

注意:若僅僅調用 pushState 方法或 replaceState 方法 ,並不會觸發該事件,只有用戶點擊瀏覽器倒退按鈕和前進按鈕,或者使用 JavaScript 調用 backforwardgo 方法時纔會觸發。另外,該事件只針對同一個文檔,若是瀏覽歷史的切換,致使加載不一樣的文檔,該事件也不會觸發。

栗子:

window.onpopstate= (event) => {
&emsp;&emsp;console.log(event.state) //當前歷史記錄的state對象
}
複製代碼

實現

瞭解了這麼多內容,那麼就讓咱們開始實現 History 模式的路由吧!

咱們將上面的 HTML 稍稍改造下,請你們耐心分析:

<body>
  <h1 id="id">我是空白頁</h1>
  <a class="route" href="/id1">id1</a>
  <a class="route" href="/id2">id2</a>
  <a class="route" href="/id3">id3</a>
</body>
複製代碼
import demo1 from './demo1.js'
  // 建立一個 newRouter 類
  class newRouter {
    // 初始化路由信息
    constructor() {
      this.routes = {};
      this.currentUrl = '';
    }
    route(path, callback) {
      this.routes[path] = (type) => {
        if (type === 1) history.pushState( { path }, path, path );
        if (type === 2) history.replaceState( { path }, path, path );
        callback()
      };
    }
    refresh(path, type) {
      this.routes[this.currentUrl] && this.routes[this.currentUrl](type);
    }
    init() {
      window.addEventListener('load', () => {
        // 獲取當前 URL 路徑
        this.currentUrl = location.href.slice(location.href.indexOf('/', 8))
        this.refresh(this.currentUrl, 2)
      }, false);
      window.addEventListener('popstate', () => {
        this.currentUrl = history.state.path
        this.refresh(this.currentUrl, 2)
      }, false);
      const links = document.querySelectorAll('.route')
      links.forEach((item) => {
        // 覆蓋 a 標籤的 click 事件,防止默認跳轉行爲
        item.onclick = (e) => {
          e.preventDefault()
          // 獲取修改以後的 URL
          this.currentUrl = e.target.getAttribute('href')
          // 渲染
          this.refresh(this.currentUrl, 2)
        }
      })
    }
  }
  // new 一個 Router 實例
  window.Router = new newRouter();
  // 實例初始化
  window.Router.init();

  // 獲取關鍵節點
  var content = document.querySelector('#id');

  Router.route('/id1', () => {
    content.innerHTML = 'id1'
  });
  Router.route('/id2', () => {
    content.innerHTML = demo1
  });
  Router.route('/id3', () => {
    $.ajax({
      url: './demo2.html',
      success: (res) => {
        content.innerHTML = res
      }
    })
  });
複製代碼

演示圖以下所示:

img

總結

通常場景下,hashhistory 均可以,除非你更在乎顏值,# 符號夾雜在 URL 裏看起來確實有些不太美麗。 另外,根據 Mozilla Develop Network 的介紹,調用 history.pushState() 相比於直接修改 hash,存在如下優點:

  • pushState() 設置的新 URL 能夠是與當前 URL 同源的任意 URL;而 hash 只可修改 # 後面的部分,所以只能設置與當前 URL 同文檔的 URL
  • pushState() 設置的新 URL 能夠與當前 URL 如出一轍,這樣也會把記錄添加到棧中;而 hash 設置的新值必須與原來不同纔會觸發動做將記錄添加到棧中
  • pushState() 經過 stateObject 參數能夠添加任意類型的數據到記錄中;而 hash 只可添加短字符串;
  • pushState() 可額外設置 title 屬性供後續使用。

這麼一看 history 模式充滿了 happy,感受徹底能夠替代 hash 模式,但其實 history 也不是樣樣都好,雖然在瀏覽器裏遊刃有餘,但真要經過 URL 向後端發起 HTTP 請求時,二者的差別就來了。尤爲在用戶手動輸入 URL 後回車,或者刷新(重啓)瀏覽器的時候。

  • hash 模式下,僅 hash 符號以前的內容會被包含在請求中,如 http://www.qqq.com,所以對於後端來講,即便沒有作到對路由的全覆蓋,也不會返回 404 錯誤。
  • history 模式下,前端的 URL 必須和實際向後端發起請求的 URL 一致,如 http://www.qqq.com/book/id。若是後端缺乏對 /book/id 的路由處理,將返回 404 錯誤。Vue-Router 官網裏如此描述:「不過這種模式要玩好,還須要後臺配置支持……因此呢,你要在服務端增長一個覆蓋全部狀況的候選資源:若是 URL 匹配不到任何靜態資源,則應該返回同一個 index.html 頁面,這個頁面就是你 app 依賴的頁面。」
  • 需在後端(ApacheNginx)進行簡單的路由配置,同時搭配前端路由的 404 頁面支持。

最後很差意思推廣一下我基於 Taro 框架寫的組件庫:MP-ColorUI

能夠順手 star 一下我就很開心啦,謝謝你們。

點這裏是文檔

點這裏是 GitHUb 地址

相關文章
相關標籤/搜索