若是你想在 web 應用實現相似 pjax 的功能特性,每每須要作一些準備,好比對於不支持 history.pushState 方法的部分瀏覽器,怎樣去作優雅降級,以知足頁面總體的可用性等等。這篇文章主要來講說 pjax 相關的問題和思路。
html
首先,由於咱們必然會用到 ajax 來搞定數據,在 js 中執行的請求和 DOM 操做並不會被 history 記錄(這麼說雖然不嚴謹,幫助理解就好);
html5
其次,單頁面應用場景(或者某一個頁面有多個交互狀態的狀況)下,瀏覽器的前進後退功能沒法獲取到某一次 ajax 操做或者交互的狀態;
react
第三(你覺得我會說最後?so cute!),接前面所述,當頁面在某種狀態下被分享或者傳播時,新的用戶進入後,頁面本應該維持在上個用戶分享或傳播時的狀態(好比你常常在朋友圈分享的各類活動頁面等等)...
jquery
基於以上且不限於以上所述的種種需求,pjax 的策略便應運而生。web
參考上面的示意圖,用一種簡單的方式來描述這個機制的過程:
ajax
首先,在執行 ajax 操做時,咱們使用 pushState 方法向 瀏覽器的 history 對象中寫入一個特定的狀態值(一組參數),保證每一次 ajax 請求都能有一個相應的 history 記錄(history.state);
json
那麼以後,當咱們訪問 history 的不一樣狀態的時候(好比點擊瀏覽器前進、後退按鈕),經過當前狀態值咱們也能找到與之對應的 ajax 操做。
瀏覽器
這裏 pushState 方法的一個好處,就是能夠在不重載頁面的狀況下,改寫瀏覽器地址欄 url(同時改變
window.location.href)。react-router
Pjax 給咱們提供了一個方案,而不只僅是 pjax 的自己內容。咱們至少能夠從兩個方面來拓展一下:
架構
(1)若是沒有 pushState,能夠用其餘方式來影響瀏覽器的歷史記錄嗎?
若是你比較瞭解 React 或者 Angular 的 router 實現,那麼這個問題很容易理解。好比 react-router 給予咱們兩種選擇,一種是基於 history.pushState 的路由實現,一種是基於 location.hash 的實現,後者相對前者而言,適用性更強一些,畢竟 錨點 這個東西,在 web1.0 時代咱們就很熟悉了。使用 location.hash 可以知足低版本瀏覽器的須要。
(2)若是把 ajax 操做換成其餘操做呢?好比通常的 DOM 操做
如此看來,借鑑於 pjax 的機制和原理,咱們能幹的事情不少。對於須要讓瀏覽器記錄的事件操做或者狀態,咱們按這個套路實現就行了。
基於上面的討論,若是你已經有種想作點什麼的衝動。那麼,我想咱們已經產生了共鳴。
看到這裏,不妨給文章點個贊或者丟幾個硬幣什麼的,十分感激 (Xie-Xie-Ba-Ba)
拋開單純的 pjax 實現(好比 jquery-pjax 等等)
若是咱們能夠本身作一個小工具(方法類庫之類的)
利用瀏覽器的 history 來驅動頁面的操做或者行爲
解決更多的問題
或者實現一個全新的功能
是否是很 cool ?
這個小標題看起來可能的有點中二(或者有點標題黨吧)。。。
從需求出發來考慮設計實現(需求驅動),是培養架構能力的好習慣。(~嚶~嚶~嚶)
(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 方法傳入一個回調,回調的內容就是咱們想要觸發的操做 */
綜合考慮一個實際的應用場景,好比咱們想要用本身構造的這種類 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}); }
從上一小節的需求出發,咱們來看一看這個小工具(包)的具體實現。
這裏直接看代碼,行文思路和具體方法的用法,能夠參考代碼註釋:
/* 基於 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 語句去掉便可。
原創不易,轉稿請註明做者、出處