一、背景
用戶點擊瀏覽器工具欄中的后退按鈕,或者移動設備上的返回鍵時,或者JS執行history.go(-1);
時,瀏覽器會在當前窗口“打開”歷史紀錄中的前一個頁面。不同的瀏覽器在“打開”前一個頁面的表現上並不統一,這和瀏覽器的實現以及頁面本身的設置都有關系。
在移動端HTML5瀏覽器和webview中,“后退到前一個頁面”意味着:前一個頁面的html/js/css等靜態資源的請求(甚至是ajax動態接口請求)根本不會重新發送,直接使用緩存的響應,而不管這些靜態資源響應的緩存策略是否被設置了禁用狀態。除非請求是JS發送的,且每次發送時,都在url中加入了隨機數。(證據可以通過抓包來看,發生后退返回時,沒截獲到主頁面、靜態資源和動態接口的請求,但抓到JS發送的pv日志請求,因為pv日志請求的url中加了隨機數。)
后退返回到上一個頁面的表現,一句話總結就是:html/js/css/接口等資源直接使用前一次請求過的,而JS中的代碼從頭開始重新執行了一遍。這在一些場景下會導致嚴重的bug,所以才會提出“后退刷新”的需求。
“后退刷新”的目標是瀏覽器在后退返回到前一個頁面時,能從server端請求到一個全新的的頁面內容(即status code 200 ok或status code 304 not modified的頁面響應,而不是status 200 from cache根本不向server端請求)進行加載展示並重新執行JS代碼。
二、思路和方案
2.1 瀏覽器歷史紀錄和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
瀏覽器在前進后退到該頁面時,就會重新發送請求。
代碼示例如下:
在jsp模板的header部分加入如下的禁用緩存設置:
- <head>
- <meta http-equiv="Pragma" content="no-cache">
- <meta http-equiv="Cache-control" content="no-cache,no-store,must-revalidate">
- <meta http-equiv="Expires" content="0">
- <%
- response.setHeader("Cache-Control","no-cache,no-store,must-revalidate");
- response.setHeader("Expires", "0");
- response.setHeader("Pragma","no-cache");
- %>
- </head>
說明一下,其實通過meta標簽設置緩存策略,會被瀏覽器忽略,設置在response header中是比較可靠的。
這樣看上去,瀏覽器歷史紀錄和HTTP緩存是有關系的。事實上不是這樣的,參考You Do Not Understand Browser History, 里面的結論是:
The browser does not respect HTTP caching rules when you click the back button.
2.2 bfcache和page cache
bfcache和page cache是webkit和firefox有一項優化技術。可參考:
- Using_Firefox_1.5_caching
- 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屬性。
實際觀察中發現,一些移動端瀏覽器的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
代碼示例如下:
- if(Q.ua.IOS){
- Q.$(window).on("pagehide",function(){
- var $body = $(document.body);
- $body.children().remove(); // wait for this callback to finish executing and then...
- setTimeout(function() {
- $body.append("<script type='text/javascript'>window.location.reload(true);<\/script>");
- });
- });
- }
- //for android qq browser
- Q.$(window).on('pageshow', function(evt){
- setTimeout(function(){
- if(evt.persisted){
- location.reload(true);
- }
- });
- });
3. 安卓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 -->
以上代碼應該放在body中,最好是作為body的第一個子元素,這樣后退發生時可以第一時間進行后退行為檢測,避免用戶看到頁面呈現,然后頁面又重新刷新,中間閃現“空白”。
以上是安卓webview的后退刷新方案,對於安卓微信內嵌瀏覽器,也適用的。
三、總結
1. PC瀏覽器,設置禁用頁面緩存header即可實現后退刷新
2. 支持bfcache/page cache的移動端瀏覽器,JS監聽pageshow/pagehide,在檢測到后退時強制刷新
3. 在前2個方案都不work的情況下,可以在HTML中寫入服務端頁面生成版本號,與本地存儲中的版本號對比判斷是否發生了后退並使用緩存中的頁面
四、花絮
1. 后退時表單控件用戶輸入內容
后退時,有些瀏覽器還會把表單控件中用戶輸入內容給記錄並恢復。可以通過給form或者input添加autocomplete="off"
屬性解決。如果不work,參考chrome和firefox中autocomplete屬性失效的解決方法
1,form中沒有input[type=password],autocomplete="off"將起作用
2,去掉form,設置input[type=text]的autocomplete也起作用
2. 從webview的角度看緩存的問題
關於安卓微信webview緩存的問題,X5內核咨詢匯總里面有描述:
8、瀏覽器緩存靜態頁面,如果這個靜態頁面重新生成了之后,還是原來的地址,用戶看到的是不是就還是老頁面了
回答:瀏覽器對頁面這種主資源沒有做緩存。只對子資源,圖片,JS,CSS等做了緩存,而且緩存都是有一個新舊對比的,這個主要是服務器來控制哪些可以緩存哪些不能緩存。會有304 這種判斷。
2、緩存模式(5種)
LOAD_CACHE_ONLY: 不使用網絡,只讀取本地緩存數據
LOAD_DEFAULT: 根據cache-control決定是否從網絡上取數據。
LOAD_CACHE_NORMAL: API level 17中已經廢棄, 從API level 11開始作用同LOAD_DEFAULT模式
LOAD_NO_CACHE: 不使用緩存,只從網絡獲取數據.
LOAD_CACHE_ELSE_NETWORK,只要本地有,無論是否過期,或者no-cache,都使用緩存中的數據。
如:www.taobao.com的cache-control為no-cache,在模式LOAD_DEFAULT下,無論如何都會從網絡上取數據,如果沒有網絡,就會出現錯誤頁面;在LOAD_CACHE_ELSE_NETWORK模式下,無論是否有網絡,只要本地有緩存,都使用緩存。本地沒有緩存時才從網絡上獲取。
www.360.com.cn的cache-control為max-age=60,在兩種模式下都使用本地緩存數據。
總結:根據以上兩種模式,建議緩存策略為,判斷是否有網絡,有的話,使用LOAD_DEFAULT,無網絡時,使用LOAD_CACHE_ELSE_NETWORK。
看上去修改webview的配置就能輕松愉快地解決問題的。只是迫於app版本發布緩慢的現實,才從JS和網頁的層面來解決這個問題。