【深刻吧,HTML 5】 性能 & 集成 —— History API

博客 有更多精品文章喲。javascript

前言

在深刻了解 History API 以前,咱們須要討論一下前端路由;路由指的是經過不一樣 URL 展現不一樣頁面或者內容的功能,這個概念最初是由後端提出的,所以,在傳統的 Web 開發模式中,路由都是服務器來控制和管理的。css

既然已經有了後端路由,爲何還須要前端路由呢?咱們知道跳轉頁面實際上就是爲了展現那個頁面的內容,那麼不管是選擇 AJAX 異步的方式獲取數據仍是將頁面內容保存在本地,都是爲了讓頁面之間的交互沒必要每次都刷新頁面,這樣用戶體驗會有極大的提高,也就能被稱爲 SPA(單頁面應用)了;可是,不夠完美,由於這種場景下缺乏路由功能,因此會致使用戶屢次獲取頁面以後,不當心刷新當前頁面,會直接退回到頁面的 初始狀態,用戶體驗極差。html

那麼前端路由是怎樣解決改變頁面內容的同時改變 URL 並保持頁面不刷新呢?這就引出了咱們這篇文章的主題:History API前端

History API

DOM window 對象經過 history 對象提供了對 當前會話(標籤頁或者 frame)瀏覽歷史的訪問,在 HTML4 的時候咱們已經可以操縱瀏覽歷史向前或向後跳轉了;當時,咱們可以使用的屬性和方法有下面這些:java

  • window.history.length:返回當前會話瀏覽過的頁面數量。
  • window.history.go(?delta):接受一個整數做爲參數,按照當前頁面在會話瀏覽歷史記錄中的位置爲基準進行移動。若是參數爲 0 或 undefined、null、false,將刷新頁面,至關於執行 window.location.reload()。若是在運行這個方法的過程當中,發現移動後會超出會話瀏覽歷史記錄的邊界時,將沒有任何效果,而且也不會報錯。
  • window.history.back():移動到上一頁,至關於點擊瀏覽器的後退按鈕,等價於 window.history.go(-1)
  • window.history.forward():移動到下一頁,至關於點擊瀏覽器的前進按鈕,等價於 window.history.go(1)

window.history.back()window.history.forward() 就是經過 window.history.go(?delta) 實現的,所以,若是沒有上一頁或者下一頁,那表示會超出邊界,因此它們的處理方式和 window.history.go(?delta) 是同樣的。node

HTML4 的時候並無可以改變 URL 的 API;可是,從 HTML5 開始,History API 新增了操做會話瀏覽歷史記錄的功能。如下是新增的屬性和方法:react

  • window.history.state:這個參數是隻讀的,表示與會話瀏覽歷史的當前記錄相關聯的狀態對象。
  • window.history.pushState(data, title, ?url):在會話瀏覽歷史記錄中添加一條記錄。如下是方法的參數詳情:
    • data(狀態對象):是一個能被序列化的任何東西,例如 object、array、string、null 等。爲了方便用戶從新載入時使用,狀態對象會在序列化以後保存在本地;此外,序列化以後 的狀態對象根據瀏覽器的不一樣有不同的大小限制(注意:規範 並無說須要限制大小),若是超出,將會拋出異常。
    • title(頁面標題):當前全部的瀏覽器都會忽略這個參數,所以能夠置爲空字符串。
    • url(頁面地址):若是新的 URL 不是絕對路徑,那麼將會相對於當前 URL 處理;而且,新的 URL 必須與當前 URL 同源,不然將拋出錯誤。另外,該參數是可選的,默認爲當前頁面地址。
  • window.history.replaceState(data, title, ?url):與 window.history.pushState(data, title, ?url) 相似,區別在於 replaceState 將修改會話瀏覽歷史的當前記錄,而不是新增一條記錄;可是,須要注意:調用 replaceState 方法仍是會在 全局 瀏覽歷史記錄中建立新記錄 。

調用 pushStatereplaceState 方法以後,地址欄會更改 URL,卻不會當即加載新的頁面,等到用戶從新載入時,纔會真正進行加載。所以,同源的目的 是爲了防止惡意代碼讓用戶覺得本身處於另外一個頁面。git

popstate 事件

每當用戶導航會話瀏覽歷史的記錄時,就會觸發 popstate 事件;例如,用戶點擊瀏覽器的倒退和前進按鈕;固然這些操做在 JavaScript 中也有對應的 window.history.back()window.history.forward()window.history.go(?delta) 方法可以達到一樣的效果。github

User navigation

若是導航到的記錄是由 window.history.pushState(data, title, ?url) 建立或者 window.history.replaceState(data, title, ?url) 修改的,那麼 popstate 事件對象的 state 屬性將包含導航到的記錄的狀態對象的一個 拷貝segmentfault

Jump to pushState

另外,若是用戶在地址欄中 手動 修改 hash 或者經過寫入 window.location.hash 的方式來 模擬用戶 行爲,那麼也會觸發 popstate 事件,而且還會在會話瀏覽歷史中新增一條記錄。須要注意的是,在調用 window.history.pushState(data, title, ?url) 時,若是 url 參數中有 hash,並不會觸發這一條規則;由於咱們要知道,pushState 只是致使會話瀏覽歷史的記錄發生變化,讓地址欄有所反應,並非 用戶導航 或者經過腳原本 模擬用戶 的行爲。

Jump to hash

獲取當前狀態對象

在介紹 HTML5 中 history 對象新增的屬性和方法時,有說道 window.history.state 屬性,經過它咱們也能獲得 popstate 事件觸發時獲取的狀態對象。

在用戶從新載入頁面時,popstate 事件並不會觸發,所以,想要獲取會話瀏覽歷史的當前記錄的狀態對象,只能經過 window.history.state 屬性。

Location 對象

Location 對象提供了 URL 相關的信息和操做方法,經過 document.locationwindow.location 屬性都能訪問這個對象。

History API 和 Location 對象其實是經過地址欄中的 URL 關聯 的,由於 Location 對象的值始終與地址欄中的 URL 保持一致,因此當咱們操做會話瀏覽歷史的記錄時,Location 對象也會隨之更改;固然,咱們修改 Location 對象,也會觸發瀏覽器執行相應操做而且改變地址欄中的 URL。

屬性

Location 對象提供如下屬性:

  • window.location.href:完整的 URL;http://username:password@www.test.com:8080/test/index.html?id=1&name=test#test
  • window.location.protocol:當前 URL 的協議,包括 :http:
  • window.location.host:主機名和端口號,若是端口號是 80(http)或者 443(https),那就會省略端口號,所以只會包含主機名;www.test.com:8080
  • window.location.hostname:主機名;www.test.com
  • window.location.port:端口號;8080
  • window.location.pathname:URL 的路徑部分,從 / 開始;/test/index.html
  • window.location.search:查詢參數,從 ? 開始;?id=1&name=test
  • window.location.hash:片斷標識符,從 # 開始;#test
  • window.location.username:域名前的用戶名;username
  • window.location.password:域名前的密碼;password
  • window.location.origin:只讀,包含 URL 的協議、主機名和端口號;http://username:password@www.test.com:8080

除了 window.location.origin 以外,其餘屬性都是可讀寫的;所以,改變屬性的值能讓頁面作出相應變化。例如對 window.location.href 寫入新的 URL,瀏覽器就會當即跳轉到相應頁面;另外,改變 window.location 也能達到一樣的效果。

// window.location = 'https://www.example.com';
window.location.href = 'https://www.example.com';
複製代碼

須要注意的是,若是想要在同一標籤頁下的不一樣 frame(例如父窗口和子窗口)之間 跨域 改寫 URL,那麼只能經過 window.location.href 屬性,其餘的屬性寫入都會拋出跨域錯誤。

Demo

window.location.href cross domain

window.location.href cross domain error

改變 hash

改變 hash 並不會觸發頁面跳轉,由於 hash 連接的是當前頁面中的某個片斷,因此若是 hash 有變化,那麼頁面將會滾動到 hash 所連接的位置;固然,頁面中若是 不存在 hash 對應的片斷,則沒有 任何效果。這和 window.history.pushState(data, title, ?url) 方法很是相似,都能在不刷新頁面的狀況下更改 URL;所以,咱們也可使用 hash 來實現前端路由,可是 hash 相比 pushState 來講有如下缺點:

  • hash 只能修改 URL 的片斷標識符部分,而且必須從 # 開始;而 pushState 卻能修改路徑、查詢參數和片斷標識符;所以,在新增會話瀏覽歷史的記錄時,pushState 比起 hash 來講更符合之前後端路由的訪問方式,也更加優雅。

    // hash
    http://www.example.com/#/example
    
    // pushState
    http://www.example.com/example
    複製代碼
  • hash 必須與原先的值不一樣,才能新增會話瀏覽歷史的記錄;而 pushState 卻能新增相同 URL 的記錄。

  • hash 想爲新增的會話瀏覽歷史記錄關聯數據,只能經過字符串的形式放入 URL 中;而 pushState 方法卻能關聯全部能被序列化的數據。

  • hash 不能修改頁面標題,雖然 pushState 如今設置的標題會被瀏覽器忽略,可是並不表明之後不會支持。

hashchange 事件

咱們能夠經過 hashchange 事件監聽 hash 的變化,這個事件會在用戶導航到有 hash 的記錄時觸發,它的事件對象將包含 hash 改變前的 oldURL 屬性和 hash 改變後的 newURL 屬性。

另外,hashchange 事件與 popstate 事件同樣也不會經過 window.history.pushState(data, title, ?url) 觸發。

hashchange

方法

Location 對象提供如下方法:

  • window.location.assign(url) 方法接受一個 URL 字符串做爲參數,使得瀏覽器馬上跳轉到新的 URL。

    document.location.assign('http://www.example.com');
    // or
    // document.location = 'http://www.example.com';
    複製代碼
  • window.location.replace(url) 方法與window.location.assign(url) 實現同樣的功能,區別在於 replace 方法執行後跳轉的 URL 會 覆蓋 瀏覽歷史中的當前記錄,所以原先的當前記錄就在瀏覽歷史中 刪除 了。

  • window.location.reload(boolean) 方法使得瀏覽器從新加載當前 URL。若是該方法沒有接受值或值爲 false,那麼就至關於用戶點擊瀏覽器的刷新按鈕,這將致使瀏覽器 拉取緩存 中的頁面;固然,若是沒有緩存,那就會像執行 window.location.reload(true) 同樣,從新請求 頁面。

  • window.location.toString() 方法返回整個 URL 字符串。

    window.location.toString();
    // or
    // window.location.href;
    複製代碼

路由實現

在使用 History API 實現路由時,咱們要注意這個 API 裏的方法(pushStatereplaceState)在改變 URL 時,並不會觸發事件;所以想要像 hash 同樣 只經過 事件(hashchange)實現路由是不太可能了。

既然如此,咱們就須要知道哪些方式可以觸發 URL 的更新了;在單頁面應用中,URL 改變只能由下面三種狀況引發:

  1. 點擊瀏覽器的前進或後退按鈕。
  2. 點擊 a 標籤。
  3. 調用 pushState 或者 replaceState 方法。

對於用戶手動點擊瀏覽器的前進或後退按鈕的操做,經過監聽 popstate 事件,咱們就能知道 URL 是否改變了;點擊 a 標籤實際上也是調用了 pushState 或者 replaceState 方法,只不過由於 a 標籤會有 默認行爲,因此須要阻止它,以免進行跳轉。

Demo

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>前端路由實現</title>
  <style> .link { color: #00f; cursor: pointer; } .link:hover { text-decoration: underline; } </style>
</head>
<body>
  <ul>
    <li><a class="link" data-href="/111">111</a></li>
    <li><a class="link" data-href="/222">222</a></li>
    <li><a class="link" data-href="/333">333</a></li>
  </ul>

  <div id="content"></div>

  <script src="./router.js"></script>
  <script> // 建立實例 const router = new Router(); const contentDOM = document.querySelector('#content'); // 註冊路由 router.route('/111', state => { contentDOM.innerHTML = '111'; }); router.route('/222', state => { contentDOM.innerHTML = '222'; }); router.route('/333', state => { contentDOM.innerHTML = '333'; }); </script>
</body>
</html>
複製代碼
// router.js

const noop = () => undefined;

class Router {
  constructor() {
    this.init();
  }

  // 初始化
  init() {
    this.routes = {};
    this.listen();
    this.bindLink();
  }

  // 所有的監聽事件
  listen() {
    window.addEventListener('DOMContentLoaded', this.listenEventInstance.bind(this));
    window.addEventListener('popstate', this.listenEventInstance.bind(this));
  }

  unlisten() {
    window.removeEventListener('DOMContentLoaded', this.listenEventInstance);
    window.removeEventListener('popstate', this.listenEventInstance);
  }

  // 監聽事件後,觸發路由的回調
  listenEventInstance() {
    this.trigger(this.getCurrentPathname());
  };

  getCurrentPathname() {
    return window.location.pathname;
  }

  // 註冊路由
  route(pathname, callback = noop) {
    this.routes[pathname] = callback;
  }

  // 觸發回調
  trigger(pathname) {
    if (!this.routes[pathname]) {
      return;
    }
    const {state} = window.history;
    this.routes[pathname](state);
  }

  // 綁定 a 標籤,阻止默認行爲
  bindLink() {
    document.addEventListener('click', e => {
      const {target} = e;
      const {nodeName, dataset: {href}} = target;
      if (!nodeName === 'A' || !href) {
        return;
      }
      e.preventDefault();
      window.history.pushState(null, '', href);
      this.trigger(href);
    });
  }
}
複製代碼

生成 Router 的實例時,咱們須要作如下工做:

  • 初始化路由映射;這個映射實際上就是一個對象,key 是路徑名,value 是觸發的回調。
  • 監聽 popstateDOMContentLoaded 事件;在上文咱們已經知道 popstate 事件在頁面加載時並不會觸發,所以須要監聽 DOMContentLoaded 事件來觸發初始的 URL 的回調。
  • 綁定所有 a 標籤,以便咱們在阻止默認行爲以後,可以調用 pushStatereplaceState 方法來更新 URL,並觸發回調。

註冊路由其實上就是在 路由映射對象 中爲 路徑 綁定 回調,由於 URL 改變後會執行回調,因此咱們能夠在回調中改變內容;這樣一個很簡單的前端路由就實現了。

總結

到此爲止,咱們深刻的瞭解了 History API 和 Location 對象,並理清了它們之間的關係。最重要的是須要明白爲何須要前端路由以及適合在什麼樣的場景下使用;另外,咱們也經過 History API 實現了一個小巧的前端路由,雖然這個實現很簡單,可是五臟俱全,經過它能很清晰的知道像 React、Vue 之類的前端框架的路由實現原理。

參考資料

  1. Manipulating the browser history
  2. HTML5 History API 和 Location 對象剖析
  3. 技術選型 — 關於前端路由和後端路由的我的思考
  4. History 對象
  5. Location 對象,URL 對象,URLSearchParams 對象
  6. Session history and navigation
  7. 前端路由實現與 react-router 源碼分析
  8. 剖析單頁面應用路由實現原理
  9. 由淺入深地教你開發本身的 React Router v4
  10. 單頁面應用路由實現原理:以 React-Router 爲例
相關文章
相關標籤/搜索