[web前端發微] 瀟灑地操做 window.history

若是你想在 web 應用實現相似 pjax 的功能特性,每每須要作一些準備,好比對於不支持 history.pushState 方法的部分瀏覽器,怎樣去作優雅降級,以知足頁面總體的可用性等等。這篇文章主要來講說 pjax 相關的問題和思路。
html

1. Why pjax?

首先,由於咱們必然會用到 ajax 來搞定數據,在 js 中執行的請求和 DOM 操做並不會被 history 記錄(這麼說雖然不嚴謹,幫助理解就好);
html5

其次,單頁面應用場景(或者某一個頁面有多個交互狀態的狀況)下,瀏覽器的前進後退功能沒法獲取到某一次 ajax 操做或者交互的狀態;
react

第三(你覺得我會說最後?so cute!),接前面所述,當頁面在某種狀態下被分享或者傳播時,新的用戶進入後,頁面本應該維持在上個用戶分享或傳播時的狀態(好比你常常在朋友圈分享的各類活動頁面等等)...
jquery

基於以上且不限於以上所述的種種需求,pjax 的策略便應運而生。web

PJAX 機制(圖片來源:百度搜索)

2. Pjax 的機制

參考上面的示意圖,用一種簡單的方式來描述這個機制的過程:
ajax

首先,在執行 ajax 操做時,咱們使用 pushState 方法向 瀏覽器的 history 對象中寫入一個特定的狀態值(一組參數),保證每一次 ajax 請求都能有一個相應的 history 記錄(history.state);
json

那麼以後,當咱們訪問 history 的不一樣狀態的時候(好比點擊瀏覽器前進、後退按鈕),經過當前狀態值咱們也能找到與之對應的 ajax 操做。
瀏覽器

這裏 pushState 方法的一個好處,就是能夠在不重載頁面的狀況下,改寫瀏覽器地址欄 url(同時改變
window.location.href)。react-router

3. Pjax 的本質

Pjax 給咱們提供了一個方案,而不只僅是 pjax 的自己內容。咱們至少能夠從兩個方面來拓展一下:
架構

(1)若是沒有 pushState,能夠用其餘方式來影響瀏覽器的歷史記錄嗎?

若是你比較瞭解 React 或者 Angular 的 router 實現,那麼這個問題很容易理解。好比 react-router 給予咱們兩種選擇,一種是基於 history.pushState 的路由實現,一種是基於 location.hash 的實現,後者相對前者而言,適用性更強一些,畢竟 錨點 這個東西,在 web1.0 時代咱們就很熟悉了。使用 location.hash 可以知足低版本瀏覽器的須要。

(2)若是把 ajax 操做換成其餘操做呢?好比通常的 DOM 操做

如此看來,借鑑於 pjax 的機制和原理,咱們能幹的事情不少。對於須要讓瀏覽器記錄的事件操做或者狀態,咱們按這個套路實現就行了。

4. By the way, and how to do?

基於上面的討論,若是你已經有種想作點什麼的衝動。那麼,我想咱們已經產生了共鳴。

看到這裏,不妨給文章點個贊或者丟幾個硬幣什麼的,十分感激 (Xie-Xie-Ba-Ba)

拋開單純的 pjax 實現(好比 jquery-pjax 等等)

若是咱們能夠本身作一個小工具(方法類庫之類的)
利用瀏覽器的 history 來驅動頁面的操做或者行爲
解決更多的問題
或者實現一個全新的功能
是否是很 cool ?

5. 慾望清單

這個小標題看起來可能的有點中二(或者有點標題黨吧)。。。

從需求出發來考慮設計實現(需求驅動),是培養架構能力的好習慣。(~嚶~嚶~嚶)

5.1 需求清單:

(1)咱們想作一個更通用的 pushState 方法,用法以下(考慮逼格,展現 ES6 語法的僞代碼):

// 以 import 形式引入依賴,easierHistory 是咱們最終構造的方法集(一個對象或構造器)或者工具包
import easierHis from './easierHistory';

// ...do something...

// 向瀏覽器歷史插入一條記錄 (例如:咱們作一個翻頁的效果時,傳入值爲一個頁碼)
easierHis.putState({page: 3});

/* 注:爲與原有 pushState 方法區別,故將新方法命名爲 putState */


(2)咱們想經過一個方法(或者接口)訪問到當前的歷史狀態(更通用的 history.state 方法):

// 獲取當前歷史狀態 state
let { state } = easierHis.getState();

/* 注:爲與原有 state 方法區別,故將新方法命名爲 getState */

(3)構造一個通用的方法,當進行瀏覽器前進後退操做時,能夠觸發一些操做:

// 獲取當前歷史狀態 state
easierHis.popState( (state) => { do something... } );

/* 注:這裏咱們給 popState 方法傳入一個回調,回調的內容就是咱們想要觸發的操做 */
5.2 一個完整的需求實例:

綜合考慮一個實際的應用場景,好比咱們想要用本身構造的這種類 pjax 機制實現一個有記錄、可前進回退的翻頁效果。大體的實現以下:

import easierHis from './easierHistory';

// 默認加載第 1 頁數據
if (!easierHis.getState()) {
  loadPage(1);      // 用於翻頁和加載數據的方法
  easierHis.putState({page: 1});
}

// 瀏覽器前進/後退時,根據 state 數據加載對應頁碼的數據
easierHis.popState((state) => {
  let cur_page = !state ? 1 : parseInt(state.page);
  loadPage(cur_page);
});

// 加載或跳轉某頁的方法
function goto(page){
  loadPage(page);
  easierHis.putState({page: page});
}

6. 具體實現

從上一小節的需求出發,咱們來看一看這個小工具(包)的具體實現。
這裏直接看代碼,行文思路和具體方法的用法,能夠參考代碼註釋:

/* 基於 ES5 的 easierHistory 實現 */
'use strict';

// 全局對象
var easierHistory = {};

/*
** @method putState : 實現 類PJAX 機制的輔助函數,用於在 history 菊花上插一刀
** @param {Object} state_content : 第 1 個參數(必填),表示當前 state 的對象字面量
** @param {Boolean} sync_prior : 第 2 個參數(選填),傳 true 則優先使用方案 $1,反之直接使用方案 $2,默認值爲 true
** @return {Object} _state : 返回 state
**
** $1 : 基於 history.pushState (絕大部分現代瀏覽器均支持)
** $2 : 經過操做 url 的 hash 字符串內容的方式來進行兼容
*/
easierHistory.putState = function (state_content, sync_prior) {
  var _state = arguments[0] || {};
  var _prior = typeof arguments[1] == 'undefined' ? true : arguments[1];

  // 拼接 search 和 hash 字符串
  var _search = '?';
  var _hash = '';
  for (var key in _state) {
    _search += key + '=' + _state[key] + '&';
    _hash += '#' + key + '=' + _state[key];
  }
  _search = _search.replace(/\&$|\?$/, '');

  // 根據瀏覽器支持狀況,選擇一種實現方式
  if (!history.pushState || !_prior) {
    location.hash = _hash;                       // $2 基於 location.hash 的實現
  } else {
    history.pushState(_state, '', _search);      // $1 基於 pushState 的實現
  }

  // 返回當前 state
  return _state;
}

/*
** @method getState_byHistory : 用於獲取 history 狀態
** @return {Object} curState : 當前 history 狀態
*/
easierHistory.getState_byHistory = function () {
  if (history.state) {
    return history.state;
  }

  if (location.search) {
    return location.search.substring(1).split('&').reduce(function (curState, queryStr) {
      if (queryStr.indexOf('=') !== -1) {
        curState[queryStr.split('=')[0]] = queryStr.split('=')[1];
      }

      return curState;
    }, {});
  }

  return null;
};

/*
** @method getState_byHash : 將 location.hash 的內容解析爲 json 對象
** @return {Object} curState : 轉換後的 json 對象
*/
easierHistory.getState_byHash = function () {
  if (!location.hash) {
    return null;
  }

  return location.hash.split('#').reduce(function (curState, hashStr) {
    if (hashStr.indexOf('=') !== -1) {
      curState[hashStr.split('=')[0]] = hashStr.split('=')[1];
    }

    return curState;
  }, {});
};

easierHistory.getState = function () {
  return easierHistory.getState_byHistory() || easierHistory.getState_byHash();
};

/*
** @method popState : 給 window對象 綁定 popState 事件,若瀏覽器不支持則向下兼容 hashchange 事件
** @param {Function} cbFunc : 事件回調
*/
easierHistory.popState = function (cbFunc) {
  if (easierHistory.getState_byHistory()) {
    window.onpopstate = function () {          // 基於 popstate 方法的實現(html5 特性)
      cbFunc(easierHistory.getState());
    };
  } else {
    window.onhashchange = function () {        // 基於 hashchange 方法的實現(兼容性更強)
      cbFunc(easierHistory.getState());
    };
  }
};


module.exports = easierHistory;

固然,上面的代碼能夠直接在瀏覽器運行(直接使用 easierHistory對象),把 module.exports 語句去掉便可。

原創不易,轉稿請註明做者、出處