38. HTML5 快取 (HTML5 Cache)

「HTML5 快取」通常指過往的應用程式快取(Application Cache),也稱為 AppCache。

它允許網站列出瀏覽器應儲存以供離線使用的資源。

AppCache 已過時,並已從現代瀏覽器移除。新網站不要使用它。

現代應用程式通常使用:

  • HTTP 快取:供一般資源重用。
  • Service Worker 與 Cache API:實作由應用程式控制的離線行為。

這些機制用途不同,不能互相替代。

38.1 過往的應用程式快取

AppCache 使用根元素 <html> 上的 manifest 屬性:

<!-- 只供了解歷史語法。新應用程式不要使用。 -->
<html lang="zh-Hant-HK" manifest="site.appcache">

所參照的檔案描述應快取哪些資源。

過往的快取清單可能包含:

CACHE MANIFEST
# Version 1

CACHE:
index.html
styles.css
app.js

NETWORK:
*

FALLBACK:
/ /offline.html

這個例子用來解釋舊資料,不是適用於現代瀏覽器的做法。

38.2 過往的清單區段

區段過往用途
CACHE:列出供離線儲存的資源
NETWORK:列出需要網絡存取的請求
FALLBACK:將 URL 命名空間對應至後備資源

清單以以下內容開始:

CACHE MANIFEST

通常使用以下回應標頭傳送:

Content-Type: text/cache-manifest

修改清單會觸發過往的更新流程。更新註解是常見的做法,用來表示新的快取版本。

AppCache 的生命週期及請求處理規則造成不少難以理解的行為,包括網頁意外維持舊版本,以及更新失敗後難以復原。因此,它被其他機制取代,而不是繼續改良。

38.3 AppCache 不是網頁應用程式資訊清單

過往的快取清單與現代的**網頁應用程式資訊清單(web app manifest)**不同:

<link rel="manifest" href="/manifest.webmanifest">

網頁應用程式資訊清單提供應用程式中繼資料,例如:

  • 應用程式名稱。
  • 圖示。
  • 啟動 URL。
  • 顯示偏好。

它本身不會快取應用程式,也不會令應用程式支援離線使用。

不要混淆:

功能用途
過往的 <html manifest="…">AppCache 設定
<link rel="manifest" href="…">網頁應用程式中繼資料
Service Worker可程式化的請求處理及其他受支援背景功能
Cache API由程式碼管理的請求與回應配對儲存

38.4 一般 HTTP 快取

HTTP 快取讓瀏覽器及中介快取按照 HTTP 規則重用回應。

伺服器透過回應標頭控制大部分行為:

Cache-Control: public, max-age=3600

這允許適用的快取將回應視為在一小時內保持新鮮。

常見指令包括:

指令一般意思
max-age設定新鮮度有效期
no-cache重用前必須驗證
no-store指示快取不要儲存回應
private阻止共用快取儲存回應
immutable表示回應在新鮮度有效期內不會改變

no-cache 不代表「不要儲存」。 如果必須禁止儲存,應使用 no-store。

ETag 和 Last-Modified 等驗證資訊可以支援條件式請求。如果已儲存的回應仍然有效,伺服器可能返回 304 Not Modified。

應透過真正的 HTTP 回應標頭設定快取,而不是使用聲稱能設定快取的 meta 標籤。

38.5 Service Worker

Service Worker 是獨立於網頁執行的程式碼,可以處理受它控制的用戶端所發出的請求。

從網頁註冊:

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/sw.js")
    .catch((error) => {
      console.error("Service Worker 註冊失敗:", error);
    });
}

重要要求及限制包括:

  • Service Worker 需要安全環境,通常是 HTTPS。
  • 一般支援在 localhost 進行本機開發。
  • Worker 程式碼必須從相同來源提供。
  • 作用範圍決定它可以控制哪些網頁。
  • 註冊不代表立即控制目前網頁。
  • Worker 不會持續執行;瀏覽器管理其存活時間。

位於 /sw.js 的 Worker,預設作用範圍通常是 /。位於子目錄的 Worker,預設作用範圍通常較窄。

只有在應用程式需要相關能力時,才使用 Service Worker。

38.6 Cache API

Cache API 在具名快取中儲存請求與回應配對:

const cache = await caches.open("course-assets-v1");

await cache.addAll([
  "/offline.html",
  "/styles.css"
]);

這些陳述必須在適當的非同步環境中執行。

Cache API 可用於 Service Worker,以及支援它的安全網頁環境。單純儲存項目不需要 Service Worker,但通常會配合它,在離線請求時提供快取內容。

重要區別包括:

  • Cache API 儲存與一般 HTTP 快取分開。
  • 已儲存的回應不會自動用於網頁請求。
  • Cache API 項目不會根據 HTTP 新鮮度標頭自動到期。
  • 應用程式必須管理更新及刪除。

不要以為帶有 Cache-Control: no-store 的回應,會自動被明確的 Cache API 儲存邏輯拒絕。應刻意避免儲存敏感回應。

38.7 常見請求策略

Service Worker 可以為不同資源選擇不同策略。

策略做法常見用途
快取優先(cache first)使用快取回應;沒有時才取得網絡資源具有版本識別的靜態資源
網絡優先(network first)先嘗試網絡;失敗時使用快取內容重視最新內容的網頁
先用舊快取再更新(stale while revalidate)返回快取內容,同時在背景更新可以接受部分過時內容的資源
只用網絡(network only)不使用應用程式管理的快取回應敏感或必須保持最新的請求

這裏的「只用網絡」,表示略過應用程式的 Cache API 策略。除非請求及回應規則另有設定,瀏覽器的一般 HTTP 快取仍然可以生效。

沒有一種策略適合所有資源。

應考慮:

  • 可以接受內容過時多久。
  • 離線存取是否有用。
  • 回應是否個人化。
  • 如何呈現錯誤。
  • 如何向使用者提供更新。

38.8 生命週期與更新

Service Worker 的生命週期包括安裝、等待及啟用。

安裝期間,應用程式可以準備必要的快取資源。

如果舊 Worker 仍然控制已開啟網頁,新 Worker 可能會等待。這有助避免在使用期間意外改變應用程式的請求行為。

啟用期間,應用程式通常會移除過時快取。

skipWaiting() 和 clients.claim() 等方法可以加快採用新 Worker,但沒有更新計劃就使用它們,可能令舊網頁配合新的資源處理行為。

應將更新視為應用程式設計的一部分,而不是事後補充。

一致地管理資源及快取版本,並在有需要時提供適當的重新載入或更新體驗。

38.9 完整範例:離線後備 Worker

以下可重用 Worker 嘗試正常的網頁導覽請求,並在網絡請求失敗時提供已快取的離線頁面。

它刻意不會快取已瀏覽網頁、需要身分驗證的回應、API 結果或其他靜態資源。

註冊 Worker 前,應先建立獨立完整的 /offline.html。必要樣式應放在頁面內,或另外加入安裝快取。

id: hong-kong-html-cache-service-worker
name: HTML5 快取替代方案 — 離線後備 Worker
type: code.javascript
content: |-
  // 儲存為 /sw.js,並透過 HTTPS 或 localhost 提供。
  // 註冊這個 Worker 前,先建立 /offline.html。

  const CACHE_NAME = "course-offline-v1";
  const CACHE_PREFIX = "course-offline-";
  const OFFLINE_URL = "/offline.html";

  self.addEventListener("install", (event) => {
    event.waitUntil(
      (async () => {
        const cache = await caches.open(CACHE_NAME);

        await cache.add(
          new Request(OFFLINE_URL, { cache: "reload" })
        );
      })()
    );
  });

  self.addEventListener("activate", (event) => {
    event.waitUntil(
      (async () => {
        const names = await caches.keys();

        await Promise.all(
          names
            .filter((name) =>
              name.startsWith(CACHE_PREFIX) &&
              name !== CACHE_NAME
            )
            .map((name) => caches.delete(name))
        );
      })()
    );
  });

  self.addEventListener("fetch", (event) => {
    const request = event.request;
    const url = new URL(request.url);

    if (
      request.method !== "GET" ||
      request.mode !== "navigate" ||
      url.origin !== self.location.origin
    ) {
      return;
    }

    event.respondWith(
      (async () => {
        try {
          return await fetch(request);
        } catch {
          const cache = await caches.open(CACHE_NAME);
          const fallback = await cache.match(OFFLINE_URL);

          if (fallback) {
            return fallback;
          }

          return new Response(
            "無法離線開啟此頁面。請重新連線後再試。",
            {
              status: 503,
              headers: {
                "Content-Type": "text/plain; charset=utf-8",
                "Cache-Control": "no-store"
              }
            }
          );
        }
      })()
    );
  });

使用第 38.5 節的程式碼註冊它。

在這個範例中:

  • 安裝時儲存離線頁面。
  • 預先快取失敗會令安裝無法成功。
  • 啟用時只刪除使用本應用程式前綴的舊快取。
  • 處理同來源的 GET 導覽請求。
  • 其他請求保留正常行為。
  • 網絡失敗時提供離線後備內容。
  • 404 和 500 等 HTTP 錯誤回應會正常返回,因為 fetch() 不會只因這些狀態而拒絕。
  • Worker 不會強制立即啟用,也不會主動接管現有網頁。

註冊後,第一個網頁可能尚未受控。Worker 啟用後,通常需要再次導覽或重新載入。

這是離線後備方案,不是完整離線應用程式。請求也可能等待一段時間後才失敗;正式應用程式可能需要明確的逾時策略。

38.10 儲存、私隱與復原

瀏覽器管理的儲存不是永久的。

快取資料可能因以下原因遺失:

  • 儲存空間不足而被清除。
  • 使用者操作。
  • 私密瀏覽限制。
  • 瀏覽器政策。
  • 應用程式清理。

不要將快取作為重要使用者資料的唯一副本。

應特別小心處理:

  • 帳戶專用網頁。
  • 個人資訊。
  • 依賴身分驗證的回應。
  • 登出後應消失的資料。

同來源快取不會自動按目前使用應用程式的帳戶分開。應設計適當儲存及清理方式,避免某個帳戶收到另一個帳戶的快取內容。

取消註冊 Service Worker,也不會自動刪除其 Cache API 項目。

應為過時或有問題的 Worker 提供復原計劃,並避免使用會刪除同來源其他應用程式快取的全面清理程式碼。

38.11 測試離線行為

不要只測試一次成功的離線載入。

應檢查:

  1. 沒有任何快取資源時的首次瀏覽。
  2. 缺少必要資源時的安裝。
  3. Worker 啟用後重新載入。
  4. 離線時的導覽。
  5. 緩慢或不穩定的連線。
  6. HTTP 錯誤回應。
  7. 部署新 Worker 版本。
  8. 多個開啟中的分頁。
  9. 儲存被清除或因空間不足而移除。
  10. 適用情況下的登出及帳戶切換。

瀏覽器開發工具通常提供面板,讓你檢查 Service Worker 及快取儲存。

開發工具設定可能改變正常生命週期,因此也應在沒有強制更新選項的情況下測試。

38.12 常見錯誤

錯誤較好的做法
使用過往的 manifest 屬性以受支援機制取代 AppCache
以為網頁應用程式資訊清單會啟用離線功能另外實作快取
將 HTTP 快取與 Cache API 儲存視為相同機制理解各自規則
以為 no-cache 禁止儲存使用適當指令
不加區分地快取所有回應根據新鮮度及私隱需要選擇資源
期望快取項目自動到期實作快取維護政策
以為註冊後立即控制網頁考慮生命週期及作用範圍
未協調網頁就強制更新 Worker設計安全更新流程
期望 fetch() 對所有 HTTP 錯誤都拒絕按需要檢查狀態
將快取儲存視為永久處理資料清除及復原
以為登出會自動清除快取資料實作按帳戶管理的清理
刪除同來源的所有快取只刪除本應用程式擁有的快取

本章總結: 過往的 HTML5 應用程式快取已經過時。一般資源重用應使用 HTTP 快取;需要刻意設計的離線行為時,使用 Service Worker 與 Cache API。應將新鮮度、更新、儲存遺失、私隱及失敗復原納入實作計劃。

Scroll to Top