測試部門開了 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 之類的 ) , 又將這個修改給擱置了, 花這麼多時間成本解決後還是妥協原本的情況......
沒有留言:
張貼留言