快取
當用戶開啟一個網站並與之互動時,網站所需的所有資源,包括 HTML、JavaScript、CSS、影像、字型以及應用明確請求的任何資料,都是透過發出 HTTP(S) 請求來獲取的。PWA 最基本的功能之一是能夠將應用的某些資源明確地快取在裝置上,這意味著無需向網路傳送請求即可獲取這些資源。
在本地快取資源主要有兩個好處:離線操作和響應速度。
- 離線操作:快取使 PWA 能夠在裝置沒有網路連線的情況下,或多或少地正常工作。
- 響應速度:即使裝置線上,如果 PWA 的使用者介面是從快取而不是從網路獲取的,其響應速度通常會快得多。
當然,其主要缺點是新鮮度:對於需要保持最新的資源,快取就不那麼適用了。此外,對於某些型別的請求,例如 POST 請求,快取是絕對不適用的。
這意味著是否以及何時快取某個資源,很大程度上取決於該資源本身,因此一個 PWA 通常會對不同的資源採用不同的策略。在本指南中,我們將探討一些常見的 PWA 快取策略,並瞭解哪種策略適用於哪種資源。
快取技術概述
PWA 建立快取策略所依賴的主要技術是 Fetch API、Service Worker API 和 Cache API。
Fetch API
Fetch API 定義了一個用於獲取網路資源的全域性函式 fetch(),以及代表網路請求和響應的 Request 和 Response 介面。fetch() 函式接受一個 Request 物件或一個 URL 作為引數,並返回一個 Promise,該 Promise 會兌現為一個 Response 物件。
fetch() 函式在 Service Worker 和應用主執行緒中都可用。
Service Worker API
Service Worker 是 PWA 的一部分:它是一個獨立的指令碼,執行在自己的執行緒中,與應用的主執行緒分離。
一旦 Service Worker 被啟用,每當應用請求一個由該 Service Worker 控制的網路資源時,瀏覽器就會在 Service Worker 的全域性作用域中觸發一個名為 fetch 的事件。這個事件不僅針對主執行緒發出的顯式 fetch() 呼叫,也針對瀏覽器在頁面導航後為載入頁面和子資源(如 JavaScript、CSS 和影像)而發出的隱式網路請求。
透過監聽 fetch 事件,Service Worker 可以攔截請求並返回一個自定義的 Response。特別是,它可以返回本地快取的響應,而不是總是訪問網路;或者在裝置離線時返回本地快取的響應。
Cache API
Cache 介面為 Request/Response 對提供了持久化儲存。它提供了新增和刪除 Request/Response 對的方法,以及查詢與給定 Request 匹配的快取 Response 的方法。快取既可以在應用主執行緒中使用,也可以在 Service Worker 中使用:因此,一個執行緒可以將響應新增到快取中,而另一個執行緒可以檢索它。
最常見的情況是,Service Worker 會在其 install 或 fetch 事件處理程式中將資源新增到快取中。
何時快取資源
PWA 可以在任何時候快取資源,但在實踐中,大多數 PWA 會在以下幾個時機選擇快取資源:
-
在 Service Worker 的
install事件處理程式中(預快取):當 Service Worker 安裝時,瀏覽器會在 Service Worker 的全域性作用域中觸發一個名為install的事件。此時,Service Worker 可以預快取資源,即從網路獲取它們並存儲在快取中。注意:Service Worker 的安裝時間與 PWA 的安裝時間不同。Service Worker 的
install事件在 Service Worker 下載並執行後立即觸發,這通常在使用者訪問你的網站時就會發生。即使使用者從未將你的網站作為 PWA 安裝,其 Service Worker 也會被安裝和啟用。
-
在 Service Worker 的
fetch事件處理程式中:當 Service Worker 的fetch事件觸發時,Service Worker 可能會將請求轉發到網路,並快取得到的響應。這可能發生在快取中尚無響應時,或為了用更新的響應來更新快取時。 -
響應使用者請求:PWA 可能會明確邀請使用者下載資源以便稍後使用,屆時裝置可能處於離線狀態。例如,音樂播放器可能會邀請使用者下載曲目以便稍後播放。在這種情況下,應用主執行緒可以獲取資源並將響應新增到快取中。特別是當請求的資源很大時,PWA 可能會使用 後臺獲取 API,此時響應將由 Service Worker 處理,並將其新增到快取中。
-
定期地:使用定期後臺同步 API,Service Worker 可以定期獲取資源並快取響應,以確保 PWA 即使在裝置離線時也能提供相當新的響應。
快取策略
快取策略是一種演算法,用於決定何時快取資源、何時提供快取的資源以及何時從網路獲取資源。在本節中,我們將總結一些常見的策略。
這並非詳盡無遺的列表:它只是為了說明 PWA 可以採取的各種方法。
快取策略需要在離線操作、響應速度和新鮮度之間取得平衡。不同的資源在這方面有不同的要求:例如,應用的基本 UI 可能相對靜態,而顯示產品列表時,擁有最新的資料可能至關重要。這意味著一個 PWA 通常會對不同的資源採用不同的策略,一個 PWA 可能會使用這裡描述的所有策略。
快取優先
在此策略中,我們將預快取一些資源,然後僅對這些資源實施“快取優先”策略。也就是說:
- 對於預快取的資源,我們將:
- 在快取中查詢資源,如果找到就返回該資源。
- 否則,訪問網路。如果網路請求成功,則將資源快取起來以備下次使用。
- 對於所有其他資源,我們將始終訪問網路。
對於 PWA 確定需要、在本應用版本中不會改變,且需要儘快獲取的資源,預快取是一種合適的策略。這包括,例如,應用的基本使用者介面。如果這些被預快取,那麼應用的 UI 可以在啟動時無需任何網路請求即可渲染。
首先,Service Worker 在其 install 事件處理程式中預快取靜態資源:
const cacheName = "MyCache_1";
const precachedResources = ["/", "/app.js", "/style.css"];
async function precache() {
const cache = await caches.open(cacheName);
return cache.addAll(precachedResources);
}
self.addEventListener("install", (event) => {
event.waitUntil(precache());
});
在 install 事件處理程式中,我們將快取操作的結果傳遞給事件的 waitUntil() 方法。這意味著如果快取因任何原因失敗,Service Worker 的安裝也會失敗;反之,如果安裝成功,Service Worker 就可以確定資源已被新增到快取中。
fetch 事件處理程式如下所示:
async function cacheFirst(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open("MyCache_1");
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
return Response.error();
}
}
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (precachedResources.includes(url.pathname)) {
event.respondWith(cacheFirst(event.request));
}
});
我們透過呼叫事件的 respondWith() 方法來返回資源。如果我們對某個請求不呼叫 respondWith(),那麼該請求就會被髮送到網路,就好像 Service Worker 沒有攔截它一樣。因此,如果一個請求沒有被預快取,它就會直接訪問網路。
當我們將 networkResponse 新增到快取時,必須克隆該響應,並將副本新增到快取中,同時返回原始響應。這是因為 Response 物件是流式的,所以只能被讀取一次。
你可能會想,為什麼我們要為預快取的資源回退到網路。既然它們被預快取了,我們難道不能確定它們一定在快取中嗎?原因是快取有可能被清除,無論是被瀏覽器還是被使用者。雖然這種情況不太可能發生,但如果發生了,PWA 除非能回退到網路,否則將無法使用。請參閱刪除快取資料。
快取優先,同時重新整理快取
“快取優先”的缺點是,一旦一個響應進入快取,直到安裝新版本的 Service Worker,它就再也不會被重新整理。
“快取優先,同時重新整理快取”策略,也稱為“stale-while-revalidate”,與“快取優先”類似,不同之處在於我們總是向網路傳送請求,即使在快取命中後也是如此,並用網路響應來重新整理快取。這意味著我們獲得了“快取優先”的響應速度,同時也能得到一個相當新的響應(只要請求被相當頻繁地發出)。
當響應速度很重要,且新鮮度有一定重要性但並非至關重要時,這是一個很好的選擇。
在這個版本中,我們對除 JSON 之外的所有資源都實施“快取優先,同時重新整理快取”策略。
function isCacheable(request) {
const url = new URL(request.url);
return !url.pathname.endsWith(".json");
}
async function cacheFirstWithRefresh(request) {
const fetchResponsePromise = fetch(request).then(async (networkResponse) => {
if (networkResponse.ok) {
const cache = await caches.open("MyCache_1");
cache.put(request, networkResponse.clone());
}
return networkResponse;
});
return (await caches.match(request)) || (await fetchResponsePromise);
}
self.addEventListener("fetch", (event) => {
if (isCacheable(event.request)) {
event.respondWith(cacheFirstWithRefresh(event.request));
}
});
請注意,我們是非同步更新快取(在 then() 處理程式中),因此應用不必等待網路響應接收完畢就可以使用快取的響應。
網路優先
我們要看的最後一個策略是“網路優先”,它是“快取優先”的逆過程:我們嘗試從網路獲取資源。如果網路請求成功,我們返回響應並更新快取。如果失敗,我們再嘗試快取。
這對於那些需要獲取儘可能新的響應,但有快取資源總比沒有好情況下的請求很有用。例如,訊息應用的最近訊息列表可能就屬於這一類。
在下面的示例中,我們對獲取應用“inbox”路徑下所有資源的請求使用“網路優先”策略。
async function networkFirst(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open("MyCache_1");
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
const cachedResponse = await caches.match(request);
return cachedResponse || Response.error();
}
}
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.pathname.match(/^\/inbox/)) {
event.respondWith(networkFirst(event.request));
}
});
仍然有些請求,沒有響應比一個可能過時的響應要好,對於這些請求,只有“僅網路”策略是合適的。例如,如果一個應用正在顯示可用產品列表,如果列表是過時的,會讓使用者感到沮喪。
刪除快取資料
快取的儲存空間有限,如果超出限制,瀏覽器可能會驅逐應用的快取資料。具體的限制和行為因瀏覽器而異:詳情請參閱儲存配額和驅逐標準。在實踐中,快取資料被驅逐的情況非常罕見。使用者也可以隨時清除應用的快取。
PWA 應該在 Service Worker 的 activate 事件中清理其快取的任何舊版本:當此事件觸發時,Service Worker 可以確定沒有先前版本的 Service Worker 在執行,因此舊的快取資料不再需要。
另見
- Service Worker API
- Fetch API
- 儲存配額和逐出標準
- Service Worker 快取策略,來自 developer.chrome.com (2021)
- 離線指南,來自 web.dev (2020)