# 淺談快取機制
為什麼需要快取?因為我們希望能夠快速獲得想要的資訊。
一般來說,前端能夠處理的快取有幾種:
- 透過 http 的 cache 機制。例如 301 以及對應的 header
- CDN
- ServiceWorker
- 透過
localStorage
存資料
以下來一一介紹。
# CDN(Content Delivery Node)
在請求一張圖片(或其他資源)時,伺服器如果架設在美國,而請求來自台灣,則 TCP 傳輸會受到延遲時間跟掉包率的影響,距離越遠當然延遲也就越大。
而 CDN 的原理用很簡單的方式來說,就是將資源快取到各個分布的節點當中,這樣使用者在請求資源時,就可以直接從離使用者最近的網路節點回應,加快回應速度。伺服器如果掛掉或某個節點掛掉了,還可以靠其他節點的快取來回應。
不過也正因為如此,要管理快取也比較麻煩一些。
如果圖片更新了怎麼辦?要怎麼讓 CDN 即時更新,這就需要一些取捨跟機制了。
最簡單的方式是,每一張圖片都給一個 hash。如 avatar.01.jpg
,當圖片更新時,我就上傳新的 avatar.02.jpg
,不過這代表你在應用程式中的檔名都需要修改,或是這個欄位是從資料庫的 URL 而來,也需要修改資料庫欄位,可能還會需要重新部署應用程式。
這樣子的好處是可以確保請求資源的即時性。
第二個方式是手動將快取清除,或者是設定比較短的過期時間,因為新的圖片要傳送到各個 CDN 節點需要時間,因此在剛開始可能一樣會有延遲。
CDN 通常也會提供一個機制讓你對資源做 invalid 或是版本管理,所以通常在做靜態資源管理時,會建議將資源放在 CDN 上,可以省下不少麻煩事。
# HTTP Header
靜態檔案可以透過 http header 提供的機制來做快取。主要的原理是透過 Cache-Control
、ETag
這兩個 header 來做對應的快取處理。
當瀏覽器遇到這個標頭時,會將靜態資源保存在 disk 當中,並根據裡頭的值設定過期時間。當請求發出時,會先確認本地檔案是否過期,再發送請求與伺服器要資料。在 Chrome 上,你可以打開開發者工具到 network 這個 tab 來查看快取來源。
詳細的行為會依照 Cache-Control 的值不同而有不同的處理,TechBridge 上的文章 (opens new window)解釋的相當詳細。
# ETag
為了確保快取檔案的正確性,我們需要一個機制來問伺服器現在快取的檔案內容是否和之前一樣,而 ETag 就是用類似 hash 的機制來產生一組編碼,比對一下 ETag,如果相同就直接回傳 304,就不用重複下載一整包相同的內容了;如果 ETag 不同再下載檔案。
要注意的是,這跟 from disk cache 不同,from disk cache 並不會發送請求,而 304 則是已經向伺服器發出請求,但不用下載檔案。
# Service Worker
ServiceWorker 除了實作 PWA 之外,你也可以把 Service Worker 當成一個 request 的 hijack。每個發送的 request 都可以透過 service worker 的 event listener 給抓起來。
透過這樣的機制,我們可以寫一個簡單的 cache。
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(res => {
if (res) {
return new Response('<p>Hijack request!</p>', {
headers: { 'Content-Type': 'text/html' }
})
}
return fetch(event.request).then(res => {
return caches.open('v1')
.then(cache => {
cache.put(event.request, res.clone())
return res
})
})
})
)
event.respondWith(
// 檢查快取中是否有可用的資源
caches.match(event.request).then( {
if(response) { // 使用 Service Worker 回應
return new Response('<p>Hello from your friendly neighbourhood service worker!</p>', {
headers: { 'Content-Type': 'text/html' }
});
} else {
console.log('No response found in cache. About to fetch from network...');
}
// Service Worker 沒有設定相對應的回應,發出 HTTP Request
return fetch(event.request).then(function(response) {
console.log('Response from network is:', response);
// 加入快取供之後使用
return caches.open('v1').then(function(cache) {
cache.put(event.request, response.clone());
return response;
});
}).catch(function(error) { // 錯誤處理
console.error('Fetching failed:', error);
throw error;
});
})
);
});
我們先看看快取裡是否有對應的資源,如果有就回傳,沒有的話再實際發請求去拿,並且放入 cache 當中。在實際開發中的情況會比上述的程式碼複雜更多,如果是用 wwebpack 可以考慮搭配 OfflinePlugin (opens new window) 來簡化邏輯。
# localStorage
你可以用 localStorage
存取任何字串。API 也相當簡單直覺 localStorage.setItem
與 localStorage.getItem
以及 localStorage.clear
。
不過使用上要考慮:
- 容量限制為 5MB,對大部分的應用程式來說應該算是夠用。
- 使用者可以在瀏覽器上禁用 WebStorage API,這會造成 localStorage 存取時跳出 Exception
- 只能以字串的形式儲存,可以用
JSON.stringify
與JSON.parse
來解析資料 - 使用者可以在 localStorage 修改、刪除、增加資料,所以要小心安全性(特別是 XSS) 的問題
- 任何第三方的程式庫都能夠存取 localStorage
- localStorage 只有 JavaScript 才能存取,不像 Cookie 可以自動由瀏覽器送出
- localStorage 也符合同源政策,意味著只有同一個 domain 下才能存取
# Memory cache
透過 JavaScript 原生的物件,你也可以寫一個簡單的 cache 來保存一些資料。但要做到這件事仍然要考慮一些事,例如:
- 快取需要考慮過期時間嗎?(雖然每次關閉 application 就沒了,但如果在 application 持續開啟的時候呢?)
- 快取需要考慮容量問題嗎?(最常見的方法是利用 LRU 來刪除)
- 快取需要在值更新後通知嗎?
根據需要可能會有不同的實作。
# 小結
There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton