前言
在我們日常的網頁瀏覽中,我們非常喜歡做一個操作:點擊瀏覽器的前進后退
在Ajax技術出現后,有些時候前進后退就會給開發者帶來困擾,甚至一些開發者試圖去干掉History
隨着Html5的發展,移動端的興旺,單頁應用出現了,於是History的處理被不得不提上議程了!
要知道,這一直是一項讓人不願意去碰的巨坑,但是單頁應用卻不得不去解決
首先History的處理邏輯看似簡單,實則復雜,稍不注意就會出問題,我們這里來探討下單頁中History的處理規則
基礎知識
javascript中History的歷史對象包含用戶已經瀏覽的URL信息,這就是我們傳說中的歷史記錄
我們一般會用到forward/back兩個方法與一個length接口,或者使用go具體到哪一層
后面一點,瀏覽器廠商發現History對象確實被管的過緊,於是又釋放了兩個關鍵接口,pushState以及replaceState,用於操作History對象
於是我們今天的一個重點便是這里的pushState以及replaceState,這兩位同學可以向History中壓入對象,並且在瀏覽器前進后退時會被觸發
pushState
pushState會往History中寫入一個對象,他造成的結果便是
① History length +1
② url 改變
③ 該索引History對應有一個State對象
這個時候若是點擊瀏覽器的后退,便會觸發popstate事件,將剛剛的存入數據對象讀出,這里舉一個簡單例子

<html xmlns="http://www.w3.org/1999/xhtml"><head> <title></title> <style type="text/css"> div { margin: 10px; } .msgBtn { margin: 10px; padding: 10px; border: 1px solid black; } </style> <script id="others_zepto_10rc1" type="text/javascript" class="library" src="/js/sandbox/other/zepto.min.js"></script> </head> <body> <div id="msg"> 消息框</div> <br><br> <span class="msgBtn">去第一頁</span> <span class="msgBtn">去第二頁</span> <span class="msgBtn">去第三頁</span> <script src="../../jquery-1.7.1.js" type="text/javascript"></script> <script type="text/javascript"> var _loc = location.href; function showMsg(el, msg) { el.html(msg); } window.addEventListener('popstate', function (e) { if (!e.state) return; showMsg($('#msg'), e.state); }); $('.msgBtn').click(function (e) { var msg = $(e.target).html(); showMsg($('#msg'), msg); history.pushState(msg, msg, _loc + '/' + msg); }); </script> <style></style> <script></script> <!-- Generated by RunJS (Wed May 07 18:05:27 CST 2014) 1ms --></body></html>
http://sandbox.runjs.cn/show/cspv3812
我們點擊第一頁時,往History中壓入了數據,並且往里面寫入了State對象(當前為msg),然后我們在瀏覽器后退時便會觸發popstate事件
這個時候我們的URL已經發生改變,我們在事件點觸發時便能進行操作了,我們這里的操作是改變msg的信息
所以這里我們得到的結果是
① pushState 會改變History
② 每次使用時候會為該索引的State加入我們自定義數據
③ 每次History的變化(forward、back、go)皆會導致popstate的觸發,並且將對應索引的State搞出來
④ 每次我們會根據State的信息還原當前的view,於是用戶點擊后退便有了與瀏覽器后退前進一致的感受
現在我們有個問題,原來History我們什么都不能干,現在State可存儲容量問題,因為State可存任何東西,很多用戶就會開始亂搞,這個時候其容量是否有限制

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <style type="text/css"> div { margin: 10px; } .msgBtn { margin: 10px; padding: 10px; border: 1px solid black; } </style> <script id="others_zepto_10rc1" type="text/javascript" class="library" src="/js/sandbox/other/zepto.min.js"></script> </head> <body> <div id="msg"> 消息框</div> <br /><br /> <ul id="list"></ul> <span class="msgBtn">去第一頁</span> <span class="msgBtn">去第二頁</span> <span class="msgBtn">去第三頁</span> <script src="../../jquery-1.7.1.js" type="text/javascript"></script> <script type="text/javascript"> var _loc = location.href; var doc = document.body.innerHTML; var list = $('#list'); function showMsg(el, msg) { el.html(msg); } window.addEventListener('popstate', function (e) { if (!e.state) return; showMsg($('#msg'), e.state.msg); console.log(e.state.obj); }); for (var i = 0; i < 100; i++) { var li = $('<li class="msgBtn">' + '當前第' + i + '頁' + '</li>'); list.append(li); } $('.msgBtn').on('click', function (e) { var msg = $(e.target).html(); showMsg($('#msg'), msg); doc = doc + msg; history.pushState({ msg: msg, obj: doc }, msg, _loc + '/' + msg); }); </script> </body> </html>
http://sandbox.runjs.cn/show/69oovy4b
這里存了一個較大字符串,並且搞了點擊,看了下好像問題不大,於是不予關注了,基礎知識也到此
PS:我們可以根據history.state獲取當前的狀態值,如果有的話
History與單頁的坑們
在常規的網頁中,首次進入一個網站,此時History length為1,不經過特殊處理的話,State為null
一次本標簽鏈接操作length會加1
在單頁中,基本思路也是如此,不同的是,我們一個個頁面變成,一個頁面上的一個個頁面卡片
我們現在的頁面跳轉是
A->B->C->D
說白了這個只不過是頁面上的4個dom對象來回的顯示隱藏罷了
所以我們所有的規則期望的是與History邏輯保持一致,如此惱人的回退問題便可以還給瀏覽器,比如:
A-B-C
現在我們想從C回到B,這個時候有兩個可能的動作,動作不同會造成不同的結果
一個是forward B;一個是back B
forward便會形成A-B-C-B的History隊列結果
back的話仍然是A-B-C,而且當前處於B狀態,瀏覽器前進可用
這個事實上與瀏覽器是保持一致的,比如我們由A進入B后,B頁面有一個link標簽鏈接到A
這時B返回A產生的結果便與上述類似了
比較惡心的事情往往與不按套路出牌有關,比如總有網站你一旦進去,點擊瀏覽器后退就出不來了,同樣的事情會發生在移動端
比如我現在直接由URL鏈接進列表頁,那么此時我點擊瀏覽器是不具備后退操作的,但是我們傳統的單頁應用頭部都會有一個回退按鈕
此時一剎那便2B了,因為我點擊該回退按鈕勢必是可以回到index頁面的,於是框架與瀏覽器的History便亂了
這個回退充滿玄機,他是在History小於1的時候的處理邏輯,這里我們有些時候不會往History插入新值,於是
瀏覽器看來這個時候是 B,框架的路由卻是B->A,於是我們點擊A的搜索再次進入列表頁(B),這個時候
框架:B->A->B
瀏覽器:B->B
這個時候我們優雅的點擊了瀏覽器的回退,B頁面發現自己的History大於1了,於是便執行瀏覽器的回退操作,結果全亂了
當然,一般情況下,我們不會像上面那樣做,我們會在B->A的時候往History中插入數據,讓他們保持一致性
框架:B->A->B
瀏覽器:B->A->B
情況往往沒有那么樂觀,更常見的情況是,以下場景
我們在訂單填寫頁寫完了訂單,於是點擊確認后便跳到了訂單完成頁,這個時候我們突然發現訂單完成頁上面居然有一個回退按鈕,這個時候的行為便不是我們說了算的了,可能發生的場景如下:
① 回到訂單填寫頁(可能已經失效,該行為最不可能)
② 回到產品搜索頁
③ 回到大首頁
④ 回到訂單列表頁
以上是業務邏輯的需求,但是我們手賤的情況點擊了一下瀏覽器自帶的回退,發現尼瑪哥又回來了(回到訂單頁)
於是業務邏輯與瀏覽器邏輯又壞了,這次而且壞的不輕,因為這里涉及另外一個事實!
訂單完成頁是共享的,他至少有三個入口
① 訂單填寫頁
② 用戶訂單列表
③ 復制url新頁面打開(此場景較少)
業務回退不是單純的瀏覽器回退,並且訂單列表與訂單完成還可能不是一個頻道(存在③的問題)
此情況制約於業務的需求,甚至說業務同事的代碼邏輯能力直接關聯,就一個簡單的訂單完成頁便有很多邏輯
所以實際的情況是History處理仍然是世界難題
所以現實的情況是,我們不會對History做特殊處理
另一種更加逗比的情形是:
我在A頁面進入B頁面,然后再B頁面非常2B的使用window.location回指A頁面,而A的back按鈕又是使用原生的History.back的話便死循環了
這個場景真實的發生過,我們當時有一個支付頁面需要進入到禮品卡頁面操作(跨頻道),然后禮品卡成功后直接使用window.loacation回指
支付頁,這個時候支付頁面點擊后退又回到了禮品卡頁面,而禮品卡頁面回退很2B的還是window.location,於是,結果大家都懂
上面那種場景出現的概率應該說不低,比如我們還有一個更惡心的場景是在hybrid內嵌時候發生
web頻道頁調native公共組件,於是進入native頁面,最后返回web頻道頁面(這個時候webview中的History空了)
我們這時點擊頁面卡片的后退極有可能是操作window.location,而回跳的頁面若不幸剛好是Historyback
那么他又會回來了......
好了,今天閑扯了一回History,若是您有任何處理History的方案,請不吝賜教