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


前言

在我們日常的網頁瀏覽中,我們非常喜歡做一個操作:點擊瀏覽器的前進后退
在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>
View Code

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM