[web前端發(fā)微] 瀟灑地操作 window.history

如果你想在 web 應(yīng)用實(shí)現(xiàn)類似 pjax 的功能特性,往往需要做一些準(zhǔn)備脂倦,比如對(duì)于不支持 history.pushState 方法的部分瀏覽器,怎樣去做優(yōu)雅降級(jí),以滿足頁(yè)面整體的可用性等等茎辐。這篇文章主要來(lái)說(shuō)說(shuō) pjax 相關(guān)的問(wèn)題和思路。

1. Why pjax?

首先掂恕,因?yàn)槲覀儽厝粫?huì)用到 ajax 來(lái)搞定數(shù)據(jù)拖陆,在 js 中執(zhí)行的請(qǐng)求和 DOM 操作并不會(huì)被 history 記錄(這么說(shuō)雖然不嚴(yán)謹(jǐn),幫助理解就好)懊亡;

其次依啰,單頁(yè)面應(yīng)用場(chǎng)景(或者某一個(gè)頁(yè)面有多個(gè)交互狀態(tài)的情況)下,瀏覽器的前進(jìn)后退功能無(wú)法獲取到某一次 ajax 操作或者交互的狀態(tài)斋配;

第三(你以為我會(huì)說(shuō)最后孔飒?so cute!)灌闺,接前面所述艰争,當(dāng)頁(yè)面在某種狀態(tài)下被分享或者傳播時(shí)坏瞄,新的用戶進(jìn)入后,頁(yè)面本應(yīng)該維持在上個(gè)用戶分享或傳播時(shí)的狀態(tài)(比如你經(jīng)常在朋友圈分享的各種活動(dòng)頁(yè)面等等)...

基于以上且不限于以上所述的種種需求甩卓,pjax 的策略便應(yīng)運(yùn)而生鸠匀。

PJAX 機(jī)制(圖片來(lái)源:百度搜索)

2. Pjax 的機(jī)制

參考上面的示意圖,用一種簡(jiǎn)單的方式來(lái)描述這個(gè)機(jī)制的過(guò)程:

首先逾柿,在執(zhí)行 ajax 操作時(shí)缀棍,我們使用 pushState 方法向 瀏覽器的 history 對(duì)象中寫入一個(gè)特定的狀態(tài)值(一組參數(shù)),保證每一次 ajax 請(qǐng)求都能有一個(gè)相應(yīng)的 history 記錄(history.state)机错;

那么之后爬范,當(dāng)我們?cè)L問(wèn) history 的不同狀態(tài)的時(shí)候(比如點(diǎn)擊瀏覽器前進(jìn)、后退按鈕)弱匪,通過(guò)當(dāng)前狀態(tài)值我們也能找到與之對(duì)應(yīng)的 ajax 操作青瀑。

這里 pushState 方法的一個(gè)好處,就是可以在不重載頁(yè)面的情況下萧诫,改寫瀏覽器地址欄 url(同時(shí)改變
window.location.href)斥难。

3. Pjax 的本質(zhì)

Pjax 給我們提供了一個(gè)方案,而不僅僅是 pjax 的本身內(nèi)容帘饶。我們至少可以從兩個(gè)方面來(lái)拓展一下:

(1)如果沒(méi)有 pushState哑诊,可以用其他方式來(lái)影響瀏覽器的歷史記錄嗎?

如果你比較了解 React 或者 Angular 的 router 實(shí)現(xiàn)及刻,那么這個(gè)問(wèn)題很容易理解镀裤。比如 react-router 給予我們兩種選擇,一種是基于 history.pushState 的路由實(shí)現(xiàn)缴饭,一種是基于 location.hash 的實(shí)現(xiàn)暑劝,后者相對(duì)前者而言,適用性更強(qiáng)一些茴扁,畢竟 錨點(diǎn) 這個(gè)東西铃岔,在 web1.0 時(shí)代我們就很熟悉了。使用 location.hash 能夠滿足低版本瀏覽器的需要峭火。

(2)如果把 ajax 操作換成其他操作呢毁习?比如一般的 DOM 操作

如此看來(lái),借鑒于 pjax 的機(jī)制和原理卖丸,我們能干的事情很多纺且。對(duì)于需要讓瀏覽器記錄的事件操作或者狀態(tài),我們按這個(gè)套路實(shí)現(xiàn)就好了稍浆。

4. By the way, and how to do?

基于上面的討論载碌,如果你已經(jīng)有種想做點(diǎn)什么的沖動(dòng)猜嘱。那么,我想我們已經(jīng)產(chǎn)生了共鳴嫁艇。

看到這里朗伶,不妨給文章點(diǎn)個(gè)贊或者丟幾個(gè)硬幣什么的,十分感激 (Xie-Xie-Ba-Ba)

拋開單純的 pjax 實(shí)現(xiàn)(比如 jquery-pjax 等等)
如果我們可以自己做一個(gè)小工具(方法類庫(kù)之類的)
利用瀏覽器的 history 來(lái)驅(qū)動(dòng)頁(yè)面的操作或者行為
解決更多的問(wèn)題
或者實(shí)現(xiàn)一個(gè)全新的功能
是不是很 cool 步咪?

5. 欲望清單

這個(gè)小標(biāo)題看起來(lái)可能的有點(diǎn)中二(或者有點(diǎn)標(biāo)題黨吧)论皆。。猾漫。

從需求出發(fā)來(lái)考慮設(shè)計(jì)實(shí)現(xiàn)(需求驅(qū)動(dòng))点晴,是培養(yǎng)架構(gòu)能力的好習(xí)慣。(嚶~嚶)

5.1 需求清單:

(1)我們想做一個(gè)更通用的 pushState 方法悯周,用法如下(考慮逼格粒督,展示 ES6 語(yǔ)法的偽代碼):

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

// ...do something...

// 向?yàn)g覽器歷史插入一條記錄 (例如:我們做一個(gè)翻頁(yè)的效果時(shí)禽翼,傳入值為一個(gè)頁(yè)碼)
easierHis.putState({page: 3});

/* 注:為與原有 pushState 方法區(qū)別屠橄,故將新方法命名為 putState */


(2)我們想通過(guò)一個(gè)方法(或者接口)訪問(wèn)到當(dāng)前的歷史狀態(tài)(更通用的 history.state 方法):

// 獲取當(dāng)前歷史狀態(tài) state
let { state } = easierHis.getState();

/* 注:為與原有 state 方法區(qū)別,故將新方法命名為 getState */

(3)構(gòu)造一個(gè)通用的方法捐康,當(dāng)進(jìn)行瀏覽器前進(jìn)后退操作時(shí)仇矾,可以觸發(fā)一些操作:

// 獲取當(dāng)前歷史狀態(tài) state
easierHis.popState( (state) => { do something... } );

/* 注:這里我們給 popState 方法傳入一個(gè)回調(diào),回調(diào)的內(nèi)容就是我們想要觸發(fā)的操作 */
5.2 一個(gè)完整的需求實(shí)例:

綜合考慮一個(gè)實(shí)際的應(yīng)用場(chǎng)景解总,比如我們想要用自己構(gòu)造的這種類 pjax 機(jī)制實(shí)現(xiàn)一個(gè)有記錄贮匕、可前進(jìn)回退的翻頁(yè)效果。大致的實(shí)現(xiàn)如下:

import easierHis from './easierHistory';

// 默認(rèn)加載第 1 頁(yè)數(shù)據(jù)
if (!easierHis.getState()) {
  loadPage(1);      // 用于翻頁(yè)和加載數(shù)據(jù)的方法
  easierHis.putState({page: 1});
}

// 瀏覽器前進(jìn)/后退時(shí)花枫,根據(jù) state 數(shù)據(jù)加載對(duì)應(yīng)頁(yè)碼的數(shù)據(jù)
easierHis.popState((state) => {
  let cur_page = !state ? 1 : parseInt(state.page);
  loadPage(cur_page);
});

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

6. 具體實(shí)現(xiàn)

從上一小節(jié)的需求出發(fā)刻盐,我們來(lái)看一看這個(gè)小工具(包)的具體實(shí)現(xiàn)。
這里直接看代碼劳翰,行文思路和具體方法的用法敦锌,可以參考代碼注釋:

/* 基于 ES5 的 easierHistory 實(shí)現(xiàn) */
'use strict';

// 全局對(duì)象
var easierHistory = {};

/*
** @method putState : 實(shí)現(xiàn) 類PJAX 機(jī)制的輔助函數(shù),用于在 history 菊花上插一刀
** @param {Object} state_content : 第 1 個(gè)參數(shù)(必填)佳簸,表示當(dāng)前 state 的對(duì)象字面量
** @param {Boolean} sync_prior : 第 2 個(gè)參數(shù)(選填)乙墙,傳 true 則優(yōu)先使用方案 $1,反之直接使用方案 $2生均,默認(rèn)值為 true
** @return {Object} _state : 返回 state
**
** $1 : 基于 history.pushState (絕大部分現(xiàn)代瀏覽器均支持)
** $2 : 通過(guò)操作 url 的 hash 字符串內(nèi)容的方式來(lái)進(jìn)行兼容
*/
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(/\&$|\?$/, '');

  // 根據(jù)瀏覽器支持情況听想,選擇一種實(shí)現(xiàn)方式
  if (!history.pushState || !_prior) {
    location.hash = _hash;                       // $2 基于 location.hash 的實(shí)現(xiàn)
  } else {
    history.pushState(_state, '', _search);      // $1 基于 pushState 的實(shí)現(xiàn)
  }

  // 返回當(dāng)前 state
  return _state;
}

/*
** @method getState_byHistory : 用于獲取 history 狀態(tài)
** @return {Object} curState : 當(dāng)前 history 狀態(tài)
*/
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 的內(nèi)容解析為 json 對(duì)象
** @return {Object} curState : 轉(zhuǎn)換后的 json 對(duì)象
*/
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對(duì)象 綁定 popState 事件,若瀏覽器不支持則向下兼容 hashchange 事件
** @param {Function} cbFunc : 事件回調(diào)
*/
easierHistory.popState = function (cbFunc) {
  if (easierHistory.getState_byHistory()) {
    window.onpopstate = function () {          // 基于 popstate 方法的實(shí)現(xiàn)(html5 特性)
      cbFunc(easierHistory.getState());
    };
  } else {
    window.onhashchange = function () {        // 基于 hashchange 方法的實(shí)現(xiàn)(兼容性更強(qiáng))
      cbFunc(easierHistory.getState());
    };
  }
};


module.exports = easierHistory;

當(dāng)然马胧,上面的代碼可以直接在瀏覽器運(yùn)行(直接使用 easierHistory對(duì)象)汉买,把 module.exports 語(yǔ)句去掉即可。

后記:

  1. 關(guān)于 history 的相關(guān)問(wèn)題佩脊,感興趣的各位大大可以在文章下面留言蛙粘,大家一起交流探討
  2. 博主水平有限垫卤,行文如有紕漏或錯(cuò)誤還望各位同行、前輩不吝賜教
  3. 如文章對(duì)您有所幫助出牧,望點(diǎn)贊或打賞一下穴肘,十分欣慰
  4. 原創(chuàng)不易,轉(zhuǎn)稿請(qǐng)注明作者出處
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末崔列,一起剝皮案震驚了整個(gè)濱河市梢褐,隨后出現(xiàn)的幾起案子旺遮,更是在濱河造成了極大的恐慌赵讯,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耿眉,死亡現(xiàn)場(chǎng)離奇詭異边翼,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)鸣剪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門组底,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人筐骇,你說(shuō)我怎么就攤上這事债鸡。” “怎么了铛纬?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵厌均,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我告唆,道長(zhǎng)棺弊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任擒悬,我火速辦了婚禮模她,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘懂牧。我一直安慰自己侈净,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布僧凤。 她就那樣靜靜地躺著畜侦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拼弃。 梳的紋絲不亂的頭發(fā)上夏伊,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音吻氧,去河邊找鬼溺忧。 笑死咏连,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鲁森。 我是一名探鬼主播祟滴,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼歌溉!你這毒婦竟也來(lái)了垄懂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤痛垛,失蹤者是張志新(化名)和其女友劉穎草慧,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匙头,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡漫谷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蹂析。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舔示。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖电抚,靈堂內(nèi)的尸體忽然破棺而出惕稻,到底是詐尸還是另有隱情,我是刑警寧澤蝙叛,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布俺祠,位于F島的核電站,受9級(jí)特大地震影響甥温,放射性物質(zhì)發(fā)生泄漏锻煌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一姻蚓、第九天 我趴在偏房一處隱蔽的房頂上張望宋梧。 院中可真熱鬧,春花似錦狰挡、人聲如沸捂龄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)倦沧。三九已至,卻和暖如春它匕,著一層夾襖步出監(jiān)牢的瞬間展融,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工豫柬, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留告希,地道東北人扑浸。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像燕偶,于是被迫代替她去往敵國(guó)和親喝噪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容