【單頁應用巨坑之History】細數History帶給單頁應用的噩夢

前言

在咱們平常的網頁瀏覽中,咱們很是喜歡作一個操做:點擊瀏覽器的前進後退
在Ajax技術出現後,有些時候前進後退就會給開發者帶來困擾,甚至一些開發者試圖去幹掉History
隨着Html5的發展,移動端的興旺,單頁應用出現了,因而History的處理被不得不提上議程了!
要知道,這一直是一項讓人不肯意去碰的巨坑,可是單頁應用卻不得不去解決javascript

首先History的處理邏輯看似簡單,實則複雜,稍不注意就會出問題,咱們這裏來探討下單頁中History的處理規則css

基礎知識

javascript中History的歷史對象包含用戶已經瀏覽的URL信息,這就是咱們傳說中的歷史記錄
咱們通常會用到forward/back兩個方法與一個length接口,或者使用go具體到哪一層html

後面一點,瀏覽器廠商發現History對象確實被管的過緊,因而又釋放了兩個關鍵接口,pushState以及replaceState,用於操做History對象java

因而咱們今天的一個重點即是這裏的pushState以及replaceState,這兩位同窗能夠向History中壓入對象,而且在瀏覽器前進後退時會被觸發jquery

pushState

pushState會往History中寫入一個對象,他形成的結果即是
① History length +1
② url 改變
③ 該索引History對應有一個State對象web

這個時候如果點擊瀏覽器的後退,便會觸發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>
View Code

http://sandbox.runjs.cn/show/cspv3812app

咱們點擊第一頁時,往History中壓入了數據,而且往裏面寫入了State對象(當前爲msg),而後咱們在瀏覽器後退時便會觸發popstate事件
這個時候咱們的URL已經發生改變,咱們在事件點觸發時便能進行操做了,咱們這裏的操做是改變msg的信息
因此這裏咱們獲得的結果是
① pushState 會改變History
② 每次使用時候會爲該索引的State加入咱們自定義數據
③ 每次History的變化(forward、back、go)皆會致使popstate的觸發,而且將對應索引的State搞出來
④ 每次咱們會根據State的信息還原當前的view,因而用戶點擊後退便有了與瀏覽器後退前進一致的感覺框架

如今咱們有個問題,原來History咱們什麼都不能幹,如今State可存儲容量問題,由於State可存任何東西,不少用戶就會開始亂搞,這個時候其容量是否有限制dom

<!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>
View Code

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的方案,請不吝賜教

相關文章
相關標籤/搜索