快取

當用戶開啟一個網站並與之互動時,網站所需的所有資源,包括 HTML、JavaScript、CSS、影像、字型以及應用明確請求的任何資料,都是透過發出 HTTP(S) 請求來獲取的。PWA 最基本的功能之一是能夠將應用的某些資源明確地快取在裝置上,這意味著無需向網路傳送請求即可獲取這些資源。

在本地快取資源主要有兩個好處:離線操作響應速度

  • 離線操作:快取使 PWA 能夠在裝置沒有網路連線的情況下,或多或少地正常工作。
  • 響應速度:即使裝置線上,如果 PWA 的使用者介面是從快取而不是從網路獲取的,其響應速度通常會快得多。

當然,其主要缺點是新鮮度:對於需要保持最新的資源,快取就不那麼適用了。此外,對於某些型別的請求,例如 POST 請求,快取是絕對不適用的。

這意味著是否以及何時快取某個資源,很大程度上取決於該資源本身,因此一個 PWA 通常會對不同的資源採用不同的策略。在本指南中,我們將探討一些常見的 PWA 快取策略,並瞭解哪種策略適用於哪種資源。

快取技術概述

PWA 建立快取策略所依賴的主要技術是 Fetch APIService Worker APICache API

Fetch API

Fetch API 定義了一個用於獲取網路資源的全域性函式 fetch(),以及代表網路請求和響應的 RequestResponse 介面。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 會在其 installfetch 事件處理程式中將資源新增到快取中。

何時快取資源

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 事件處理程式中預快取靜態資源:

js
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 事件處理程式如下所示:

js
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 之外的所有資源都實施“快取優先,同時重新整理快取”策略。

js
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”路徑下所有資源的請求使用“網路優先”策略。

js
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 在執行,因此舊的快取資料不再需要。

另見