2014年8月7日 星期四

Ajax 刷新 + 改變 URL + 重新整理 (F5)

最近在工作上遇到了一個困難,

測試部門開了 1 個 Bug 是關於使用者操作引發的.

 狀況描述:  
    頁面有切換頁籤與切換分頁的功能,都使採用 Ajax Get 抓取資料去刷新內容,  
    今天使用者切到頁籤 3 並按到第 5 頁...  
    Bug 1: 按重新整理後回到預設頁籤的第 1 頁  
    Bug 2: 按上一頁回到前導頁  

這是因為切換頁籤與切換分頁都時候都是使用 Ajax.

URL 並不會隨著列表刷新而改動,

所以

Bug 1 原因 : 重新整理後不會帶著最後一次的搜尋條件去 Get 正確的內容

Bug 2 原因 : URL 一直保持原狀沒有重新導向所以上一頁是回前導頁

知道原因後就要開始解決問題了...

相信這問題應該不是只有我遇到,

所以網路上應該很多神人都會提供解決辦法.

主要參考了這兩篇文章 :

不刷新改变URL: pushState + Ajax
凡走過請留下痕跡:AJAX網頁的狀態與瀏覽記錄

其實本文要探討的主體就是 : 要如何創造一個能記錄狀態的 Ajax 頁面 ?

比較古早的解決方式是使用 URL Hash 與 IFrame,

兩者的差異就是 IFrame 是 IE 的解決方式.

URL Hash


  • ajaxReload() 函式負責接收 url 並執行 ajax 代碼, 並在 ajax 結束後去修改 URL 的 Hash.
  • analyHash() 函式負責解析 Hash 並將解析完的 URL 傳至 ajaxReload().
  • window.onload 負責判斷瀏覽器是否支持 onhashchange 監聽事件
  • pollHash() 函式負責比對 Hash 有無變化, 若有變化執行 analyHash(). 

var recentHash;

//    刷新內容
function ajaxReload (url) {
    var currentStatus, currentPage;
    /* ...
    抓取頁籤 (Status) 與頁數 (Page) 執行 ajax 代碼,
    改變 Hash 不會重新導向頁面,
    如下範例為 http://l7960261.blogspot.tw/#!/Status/0/Page/1
    */
    window.location.hash = "!/Status/" + currentStatus + "/Page/" + currentPage;
};

//    解析 hash 並執行 ajax get.
function analyseHash () {
    var hash = window.location.hash;
    var url;
    //    解析 hash 將 url 組成 QueryString 代入到 ajaxReload.
    ajaxReload(url);
};

window.onload = function () {
    //    判斷該瀏覽器是否支持 onhashchange 監聽事件
    if ("onhashchange" in winodw) {
        window.onhashchange = analyseHash;
    } else {
        setInterval(pollHash, 1000);
    }
    
};

//    檢查 Hash 是否發生變化
function pollHash () {
    if (window.location.hash === recentHash) {
        return;    // Hash 沒有變化, 無需動作.
    }
    //    When URL Hash changed
    //    update the recentHash & analyseHash().
    recentHash = window.location.hash;
    analyseHash();    
};
範例中舉例使用 URL Hash 原理去做 ajax 刷新, 部分代碼已改為文字註解替代

IFrame


前述的 URL Hash 方式在比較低階的 IE7 以下是無法作用的, 因為 IE 不會將 Hash 的變化紀錄到瀏覽紀錄內. 但是在 IE 邏輯中 IFrame 的 URL 變化跟頁面 URL 一樣會被記錄至瀏覽紀錄內,  為此我們會在之後的代碼產生一個隱藏的 IFrame, 並利用這個特性與之前的代碼結合. 不過實際去寫 Code 去 Try 之後,發生了一件匪夷所思的事情, 原本想說動態產生的元素給它個初始 src = "javascript: void(0);", 結果就發生悲劇了. 在 DOM 中取 contentWindow.document 與 contentDocument 和其他相關物件都存取被拒 (Access Denied), 去掉之後就可以正常 work 了...... 還有一定要執行 iframe DOM 下的 open() 和 close(), 不然無法正常紀錄歷程 ! 再來關於存取被拒 (Access Denied) 有人解答了 : 
iframe contentWindow throws Access Denied error after shortening document.domain .


var recentHash, iframe;

window.onload = function () {
    recentHash = window.location.hash;
    //    jQuery 動態產生元素.
    //    不要加入多餘的 src="javascript: void(0);"
    //    IE 會讓 contentWindow 下很多物件存取被拒(Access Denied)
    var newIframe = $('<iframe id="history_iframe" style="display: none;"></iframe>');
    $('body').prepend(newIframe);
    //    iframe 變數參考到新加入的 DOM
    iframe = $("#history_iframe")[0].contentWindow.document;
    //    open() & close() 一定要執行, 不然改變 src 時不會記錄.
    iframe.open();
    iframe.close();
    iframe.location.hash = recentHash;
    
    analyseHash();
    setInterval(pollHash, 1000);
};

//    解析 hash 並執行 ajax get.
function analyseHash () {
    var hash = iframe.location.hash;
    var url;    
    /*
    ...解析 hash 將 url 組成 QueryString 代入到 ajaxReload.
    */
    ajaxReload(url);
};

//    Ajax 刷新
function ajaxReload(url){
    /*
    ...執行 ajax get 刷新內容
    */
};

//    檢查 iframe 內 Hash 是否發生變化
function pollHash () {
    var currentHash = iframe.location.hash;
    if (curremtHash !== recentHash) {
        location.hash = currentHash;
        recentHash = currentHash;
        analyseHash();
    }
};

function onClick(){
    //    透過改變 iframe 的 src 紀錄塞至 IE 歷史紀錄
    //    註: 上一頁/下一頁切換的是 iframe 的瀏覽紀錄
    var url;
    $('#history_iframe').attr('src', url);
    //    由於改變 src 屬性必須重新設置 iframe 參考 DOM
    iframe = $("#history_iframe")[0].contentWindow.document;
    iframe.open();
    iframe.close();
}
有網友特地去實現模仿 onhashchage : 不使用定时器实现的onhashchange 

前面落落長提了 2 個解決方法, 在 HTML5 只需要一個 History API 就能搞定了,

那為什麼不直接提這個方法呢 ? ( 總有些人還喜愛著舊瀏覽器阿 ..... )

History API


主要就介紹 pushState()replaceState()window.onpopstate

pushState(state, title, url): 把 url 塞到歷史記錄裡,名稱為 title,且此頁面保有 state物件

replaceState(state, title, url): 跟pushState用法一樣,只是此function功能是取代目前的記錄,而非新增一筆

window.onpopstate:當我們在同一個頁面的歷史紀錄中來回時會產生的event。


window.onpopstate = function(event) {
    alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // alerts "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // alerts "location: http://example.com/example.html, state: null
history.go(2);  // alerts "location: http://example.com/example.html?page=3, state: {"page":3}


History.js


再來就是有網路神人已經把跨瀏覽器的解決方法寫成了一個 History.js Library, 一個好的棒的 Library 讓你跨越瀏覽器之間的障礙, 真是太棒啦 ~~~~~~ 重點是它的使用方法幾乎跟 History API 一模一樣阿 !


(function(window,undefined){

    // Bind to StateChange Event
    History.Adapter.bind(window,'statechange',function(){ // Note: We are using statechange instead of popstate
        var State = History.getState(); // Note: We are using History.getState() instead of event.state
    });

    // Change our States
    History.pushState({state:1}, "State 1", "?state=1"); // logs {state:1}, "State 1", "?state=1"
    History.pushState({state:2}, "State 2", "?state=2"); // logs {state:2}, "State 2", "?state=2"
    History.replaceState({state:3}, "State 3", "?state=3"); // logs {state:3}, "State 3", "?state=3"
    History.pushState(null, null, "?state=4"); // logs {}, '', "?state=4"
    History.back(); // logs {state:3}, "State 3", "?state=3"
    History.back(); // logs {state:1}, "State 1", "?state=1"
    History.back(); // logs {}, "Home Page", "?"
    History.go(2); // logs {state:3}, "State 3", "?state=3"

})(window);

後端/伺服器端的支援


因為 ajax request 的 http header 會有一個 X-Requested-With 欄位為 XMLHttpRequest, 只要判斷這個欄位就可以知道是否為 ajax request.

後記


雖然在工作上漂亮解決了這個 Bug, 不過上司看到 UX( 使用者體驗 ) 不太好 ( 因為 Designer 沒有在 ajax 讀取回來的延遲時間設計畫面, 以至於畫面就像停住後突然資料跑出來. 大部分都會設計個 blockUI - Loading 之類的 ) , 又將這個修改給擱置了, 花這麼多時間成本解決後還是妥協原本的情況......

沒有留言: