以前聽過有個笑話是這樣說的:
某A:聽說 iOS 在瀏覽網頁的時候很省電
某B:對,因為它什麼事都沒做。
原來這件事是真的,根據 RFC 7234 5.2.1.4 的定義,如果我們在發送 Request 的時候,加上 cache-control: no-cache
,在沒有從伺服器成功取得內容之前,不得使用已儲存的快取來滿足目前的 Request,但是 iOS 它連 Request 都沒送,自然就不需要理會這個定義。
某天,我家老闆拿著他的 iPhone 跑來問我「為什麼我一直看到舊的資訊?而且重新整理都沒有用,必須清除瀏覽資料才可以。」,我當時心裡想「誰叫你要拿 iPhone!?」,但是身為工程師自然得找出原因是啥?
是這樣的,我們網站的頁面資料大都有使用瀏覽器的快取,避免後端伺服器一直受到騷擾,有一部分的頁面資料會因為時間到了,它必須得強制清除快取,然後從後端伺服器取得最新的資料,方法也很簡單,就是程式判斷需要強制更新的時候,在 Request Headers 中加入 cache-control: no-cache 就好了。
這個做法在 Desktop 平台、Android 平台的幾個主流瀏覽器上,都運作正常,只有 iOS 跟人家不一樣,在 iOS 上不管是 Safari、Chrome、Firefox、Edge,都失效了,我們就來看看怎麼個失效法?
開始實驗
我的實驗環境是這樣,我用 ASP.NET Core MVC 寫了一個 Action,這個 Action 有兩個路由 /test/test
、/test/test/test
,並且掛上 [ResponseCache(Duration = 600)]
,而回傳的內容也很簡單,就是一個 Timestamp 的值。
接著,我弄了一個簡單的測試頁,用 jQuery.ajax() 分別去 GET /test/test 及 /test/test/test,把內容個別顯示在兩個區塊上,有一點不同的是,在 GET /test/test/test 時,Headers 加上 cache-control: no-cache,頁面上還有一個 Refresh 按鈕用來手動更新內容。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Test</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>
<body>
<div id="cached">Cached: <span></span></div>
<div id="no-cached">NoCached: <span></span></div>
<div>
<button onclick="refresh()">Refresh</button>
</div>
<script>
$(() => {
refresh();
});
function refresh() {
$.ajax("/test/test").then(o => {
$("#cached span").text(o.value);
});
$.ajax("/test/test/test", { headers: { "cache-control": "no-cache" } }).then(o => {
$("#no-cached span").text(o.value);
});
}
</script>
</body>
</html>
正常來講,應該像下面的影片一樣,Cached 的內容不會動,NoCached 的內容每按一次就更新一次。
但是換到 iOS 的時候,NoCached 的內容就不會動了。
使用 DevTools
我分別進入 Android 及 iOS 的瀏覽器 DevTools,在 Android 上點擊 Refresh 按鈕,於 Network 頁籤會出現兩個 Request,一個顯示資料來源是 (disk cache),一個則顯示成功從伺服器取得資料,但是在 iOS 初始載入之後,不管怎麼按 Refresh 按鈕,都看不到這兩個 Request,NoCached 的內容也都不會更新。
這確定一件事情,在 iOS 上,只要我們的 Response 有指定使用瀏覽器快取,在 Headers 加入 cache-control: no-cache 來更新快取是行不通的,我們得想辦法在 iOS 上處理這個問題。
QueryString + localStorage
要從後端伺服器取得最新資料還有一招大絕,就是在 URL 後面隨便帶個 QueryString,這樣就會被視為是不同網址,重新發送 Request,通常的做法是用 Timestamp,像這樣 ?t=1616468166396
,但是這樣瀏覽器快取在 iOS 上就無用武之地了,所以我想了個方式,把 Timestamp 存放在 localStorage,當頁面內容需要強制更新的時候,就更新 localStorage 內的 Timestamp,實際做法請看下面的程式碼。
<script>
if (!window.isiOS) {
window.isiOS = function () {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
}
}
if (!window.mayTheForceBeWithiOS) {
window.mayTheForceBeWithiOS = function (prefix, refresh) {
if (!window.isiOS()) return "";
let force = window.localStorage.getItem("ios-force");
if (!force || refresh === true) {
force = Date.now();
window.localStorage.setItem("ios-force", force);
}
return `${prefix}_ios=${force}`;
}
}
$(() => {
refresh();
});
function refresh() {
$.ajax(`/test/test${window.mayTheForceBeWithiOS("?")}`).then(o => {
$("#cached span").text(o.value);
});
$.ajax(`/test/test/test${window.mayTheForceBeWithiOS("?", true)}`, { headers: { "cache-control": "no-cache" } }).then(o => {
$("#no-cached span").text(o.value);
});
}
</script>
以前聽過一句話「程式不是照你想的跑,是照你寫的跑。」,現在這句話要改一下了「程式不是照你想的跑,也不是照你寫的跑,是照 iOS 的規則跑。」