一、背景
用戶點擊瀏覽器工具欄中的后退按鈕,或者移動設備上的返回鍵時,或者JS執行history.go(-1);時,瀏覽器會在當前窗口“打開”歷史紀錄中的前一個頁面。不同的瀏覽器在“打開”前一個頁面的表現上并不統一,這和瀏覽器的實現以及頁面本身的設置都有關系。
在移動端HTML5瀏覽器和webview中,“后退到前一個頁面”意味著:前一個頁面的html/js/css等靜態資源的請求(甚至是ajax動態接口請求)根本不會重新發送,直接使用緩存的響應,而不管這些靜態資源響應的緩存策略是否被設置了禁用狀態。(這點我在自己的項目中也確實得到了驗證,按回退按鈕的時候抓包并沒有抓到任何請求)。
在我自己項目中因為涉及到存取cookie的原因,由于返回不刷新而導致一系列的bug,所以需要‘回退刷新’的需求。
“回退刷新”的目標是瀏覽器在后退返回到前一個頁面時,能從server端請求到一個全新的的頁面內容(即status code 200 ok或status code 304 not modified的頁面響應,而不是status 200 from cache根本不向server端請求)進行加載展示并重新執行JS代碼。
二、解決方案
瀏覽器歷史紀錄和HTTP 緩存
PC瀏覽器實現后退刷新的方法是給響應添加Cache-Control的header,如果server返回頁面響應的headers中包含如下內容:
Cache-Control: no-cache,no-store,must-revalidate
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
瀏覽器在前進后退到該頁面時,就會重新發送請求。
我們自己控制的話需要在頭部加相關的meta標簽
<meta http-equiv="cache-control" content="max-age=0" />
<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="expires" content="0" />
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" /> //設置頁面過期時間
<meta http-equiv="pragma" content="no-cache" /> //
或者設置響應頭
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
res.header('Expires', '-1');
res.header('Pragma', 'no-cache');
相比較而言,在header中設置比設置meta標簽更為靠譜一些,但是也存在兩者都沒效果的情況。
這樣看上去,瀏覽器歷史紀錄和HTTP緩存是有關系的。事實上不是這樣的,參考
You Do Not Understand Browser History,里面的結論是:
The browser does not respect HTTP caching rules when you click the back button.(當你點擊返回按鈕的時候瀏覽器不會遵循http緩存機制)
看來瀏覽器也是很任性的...
bfcache和page cache
bfcache和page cache是webkit和firefox有一項優化技術。可參考:
1、Using_Firefox_1.5_caching
2、WebKit Page Cache I – The Basics 和 WebKit Page Cache II – The unload Event
這里簡單介紹一下:
對于支持bfcache/page cache的瀏覽器,“后退”不光意味著html/js/css/接口等動靜態資源不會重新請求,連JS也不會重新執行。因為前一個頁面沒有被unload,最后離開時的狀態和數據被完整地保留在內存中,發生后退時瀏覽器直接把“離開時”的頁面狀態展示給用戶。
就好像,你在頁面A,點擊鏈接要在當前窗口打開頁面B,這時瀏覽器在不卸載頁面A的情況下去加載頁面B。這時你看到的是頁面B,那頁面A呢? 頁面A只是被隱藏了,JS暫停執行(我們稱之為pagehide)。如果用戶點擊“返回”,瀏覽器快速把頁面B隱藏,并把頁面A再顯示出來,JS恢復執行(我們稱之為頁面B pagehide, 頁面A pageshow)。
pageshow事件在頁面全新加載并展現時也會觸發,與從bfcache/page cache中加載并展示的區分依據是pageshow event的persisted屬性。如果從緩存獲取那么persisted的值就為true。
實際觀察中發現,一些移動端瀏覽器的pageshow event的persisted屬性值一直是false,盡管頁面看上去確實是從bfcache/page cache中加載展示。(另外一個理論上的point,頁面綁定了unload事件時,不再會進入bfcache/page cache,一些移動端瀏覽器上觀察來看實際上也不是這樣的)。
可行的方案是:JS監聽pagehide/pageshow來阻止頁面進入bfcache/page cache,或者監測到頁面從bfcache/page cache中加載展現時進行刷新。參考:
Forcing mobile Safari to re-evaluate the cached page when user presses back button。
示例代碼:
window.onpageshow = function(event) {
if (event.persisted) {
window.location.reload()
}
};
安卓webview cache的問題
安卓webview,包括安卓微信里面內嵌的QQ X5內核瀏覽器,都存在后退不會重新請求頁面的問題,無論頁面是否禁用緩存。上面的pageshow/pagehide方案也都失效。可行的方法,如下:
1. 給每個需要后退刷新的頁面上加一個hidden input,存儲頁面在服務端的生成時間,作為頁面的服務端版本號。
2. 并附加一段JS讀取讀取頁面的版本號,同時也記錄在瀏覽器/webview本地(cookie/localStorage/sessionStorage)進行存儲,作為本地版本號。
3. JS檢查頁面的服務端版本號和本地存儲中的版本號,如果服務端版本號大于本地存儲中版本號,說明頁面是從服務端重新生成的;否則頁面就是本地緩存的,即發生了后退行為。
4. JS在監測到后退時,強制頁面重新從服務端獲取。
該方案的前提是瀏覽器在向server請求頁面時,每次都用jsp重新生成html。需要頁面本身有禁用緩存的配置。
方案的代碼示例如下:
<!-- 安卓webview 后退強制刷新解決方案 START -->
<jsp:useBean id="now" class="java.util.Date" />
<input type="hidden" id="SERVER_TIME" value="${now.getTime()}"/>
<script>
//每次webview重新打開H5首頁,就把server time記錄本地存儲
var SERVER_TIME = document.getElementById("SERVER_TIME");
var REMOTE_VER = SERVER_TIME && SERVER_TIME.value;
if(REMOTE_VER){
var LOCAL_VER = sessionStorage && sessionStorage.PAGEVERSION;
if(LOCAL_VER && parseInt(LOCAL_VER) >= parseInt(REMOTE_VER)){
//說明html是從本地緩存中讀取的
location.reload(true);
}else{
//說明html是從server端重新生成的,更新LOCAL_VER
sessionStorage.PAGEVERSION = REMOTE_VER;
}
}
</script>
<!-- 安卓webview 后退強制刷新解決方案 END -->
三、總結
1. PC瀏覽器,設置禁用頁面緩存header即可實現后退刷新
2. 支持bfcache/page cache的移動端瀏覽器,JS監聽pageshow/pagehide,在檢測到后退時強制刷新
3. 在前2個方案都不work的情況下,可以在HTML中寫入服務端頁面生成版本號,與本地存儲中的版本號對比判斷是否發生了后退并使用緩存中的頁面