為什麼需要快取?因為我們希望能夠快速獲得想要的資訊。
一般來說,前端能夠處理的快取有幾種:
localStorage
存資料以下來一一介紹。
在請求一張圖片(或其他靜態資源)時,伺服器如果架設在美國,而請求來自台灣,則 TCP 傳輸會受到延遲時間跟掉包率的影響,距離越遠當然延遲也就越大。
而 CDN 的原理用很簡單的方式來說,就是將資源快取到各個分布的節點當中,這樣使用者在請求資源時,就可以直接從離使用者最近的網路節點回應,加快回應速度。伺服器如果掛掉或某個節點掛掉了,還可以靠其他節點的快取來回應。
不過也正因為如此,要管理快取也比較麻煩一些。
如果圖片更新了怎麼辦?要怎麼讓 CDN 即時更新,這就需要一些取捨跟機制了。
最簡單的方式是,每一張圖片都給一個 hash。如 avatar.01.jpg
,當圖片更新時,我就上傳新的 avatar.02.jpg
,不過這代表你在應用程式中的檔名都需要修改,或是這個欄位是從資料庫的 URL 而來,也需要修改資料庫欄位,可能還會需要重新部署應用程式。
這樣子的好處是可以確保請求資源的即時性。
第二個方式是手動將快取清除,或者是設定比較短的過期時間,因為新的圖片要傳送到各個 CDN 節點需要時間,因此在剛開始可能一樣會有延遲。
CDN 通常也會提供一個機制讓你對資源做 invalid 或是版本管理,所以通常在做靜態資源管理時,會建議將資源放在 CDN 上,可以省下不少麻煩事。
靜態檔案可以透過 http header 提供的機制來做快取。主要的原理是透過 Cache-Control
、ETag
這兩個 header 來做對應的快取處理。
當瀏覽器遇到這個標頭時,會將靜態資源保存在 disk 當中,並根據裡頭的值設定過期時間。當請求發出時,會先確認本地檔案是否過期,再發送請求與伺服器要資料。在 Chrome 上,你可以打開開發者工具到 network 這個 tab 來查看快取來源。
詳細的行為會依照 Cache-Control 的值不同而有不同的處理,TechBridge 上的文章解釋的相當詳細。
為了確保快取檔案的正確性,我們需要一個機制來問伺服器現在快取的檔案內容是否和之前一樣,而 ETag 就是用類似 hash 的機制來產生一組編碼,比對一下 ETag,如果相同就直接回傳 304,就不用重複下載一整包相同的內容了;如果 ETag 不同再下載檔案。
要注意的是,這跟 from disk cache 不同,from disk cache 並不會發送請求,而 304 則是已經向伺服器發出請求,但不用下載檔案。
ServiceWorker 除了實作 PWA 之外,你也可以把 Service Worker 當成一個 request 的攔截器。每個發送的 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 來簡化邏輯。
你可以用 localStorage
存取任何字串。API 也相當簡單直覺 localStorage.setItem
與 localStorage.getItem
以及 localStorage.clear
。
不過使用上要考慮:
JSON.stringify
與 JSON.parse
來解析資料透過 JavaScript 原生的物件,你也可以寫一個簡單的 cache 來保存一些資料。但要做到這件事仍然要考慮一些事,例如:
根據需要可能會有不同的實作。
There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton