PWA 用戶端儲存機制實作 運用 IndexedDB 與 Cache API 達成離線後備頁面

me
林彥成
2021-09-16 | 4 min.
文章目錄
  1. 1. 什麼是用戶端儲存與離線後備頁面?
  2. 2. 什麼是用戶端儲存 (Client-side storage)
  3. 3. 常見用戶端儲存機制介紹
  4. 4. 用戶端儲存限制
    1. 4.1. QuotaExceededError 錯誤處理
  5. 5. 儲存淘汰機制 (eviction)
    1. 5.1. LRU policy
    2. 5.2. Persistent Storage
  6. 6. 離線後備頁面介紹 (offline fallback page)
  7. 7. 離線後備頁面實作
    1. 7.1. Service worker 離線實作程式碼
  8. 8. 離線頁面設計建議
  9. 9. FAQ:PWA 用戶端儲存常見問題
    1. 9.1. Q1:我該在什麼時候使用 IndexedDB 而不是 LocalStorage?
    2. 9.2. Q2:瀏覽器什麼時候會主動清除我的 Cache API 資料?
    3. 9.3. Q3:離線後備頁面 (Offline Fallback) 對 SEO 有幫助嗎?

什麼是用戶端儲存與離線後備頁面?

用戶端儲存 (Client-side storage) 是指 Web 應用程式將資料(如偏好設定、靜態資源或離線檔案)直接儲存在使用者瀏覽器中的技術,主要包含 Web Storage (LocalStorage)Cache APIIndexedDB。而 離線後備頁面 (Offline Fallback Page) 則是利用 Service Worker 監聽網路請求,當偵測到使用者斷網或請求失敗時,自動回傳預先快取的 HTML 頁面。這套機制確保了 Web App 在無網路環境下仍能顯示基本的互動介面(如:離線遊戲或重試按鈕),是提升 Progressive Web App (PWA) 強韌性與可靠性的關鍵架構。


什麼是用戶端儲存 (Client-side storage)

對一個 App 來說,為了達到某些目的,將相關資料存在用戶端是相對方便的,舉例來說

  • 個人偏好設定,像是常用功能、顏色主題、字體大小
  • 將前一次操作快取,像是紀錄購物車資料或是線上編輯器在斷線時先暫時將資料保存
  • 不常改變的資料或靜態資源快取
  • 離線操作所需要的檔案

通常用戶端的儲存和伺服器端的儲存會是一起搭配使用的,舉例來說當我們使用影音串流功能時,會從伺服器端下載相關資料到用戶端使用,在過程中 App 也能夠將資料快取起來以便下次使用。

不過在實務上,瀏覽器用戶端的儲存目前有各式解決方案,也都有著各自的限制存在,如果是有大小限制的儲存,就必須注意定時和伺服器端同步去避免資料遺失。

常見用戶端儲存機制介紹

目前常見的用戶端儲存機制如下:

  • Cookies: 每次發 Request 都會一起送出,所以大小控制要注意。
  • Web Storage API:
    • SessionStorage: 同步阻塞 (synchronous),上限 5MB,僅存在於 Tab 當次操作,無法被 web workers 或 service workers 使用。
    • LocalStorage: 同步阻塞 (synchronous),上限 5MB,無法被 web workers 或 service workers 使用。
  • Cache API: 非同步,較適合跟網路請求相關的靜態資源快取。
  • IndexedDB API: 非同步,適合儲存程式邏輯相關資料,使用上相對複雜,較推薦使用像是 idb 這類相關套件操作。
  • WebSQL: 不建議使用。

用戶端儲存限制

一般來說儲存空間的上限如下:

  • Chrome: 總共最多可以用到 80% 的硬碟,每個網域最高是 60%。
  • IE 系列 10+: 最多 250MB。
  • Firefox: 硬碟空間的 50%。
  • Safari: 1GB。

在 Chrome、Firefox、Edge 等瀏覽器中可以使用以下的程式碼去估計剩餘空間:

1
2
3
4
5
6
7
8
9
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> 用了多少 bytes
// quota.quota -> 還剩多少 bytes 可使用
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`${percentageUsed}% 已使用`);
const remaining = quota.quota - quota.usage;
console.log(`還可以寫入 ${remaining} bytes`);
}

QuotaExceededError 錯誤處理

  • IndexedDB:
1
2
3
4
5
6
7
const transaction = idb.transaction(["entries"], "readwrite");
transaction.onabort = function (event) {
const error = event.target.error; // DOMException
if (error.name == "QuotaExceededError") {
// 錯誤處理邏輯
}
};
  • Cache API:
1
2
3
4
5
6
7
8
try {
const cache = await caches.open("my-cache");
await cache.add(new Request("/test.jpg"));
} catch (err) {
if (err.name === "QuotaExceededError") {
// 錯誤處理邏輯
}
}

儲存淘汰機制 (eviction)

資料在儲存上會分成兩種類別:

  • Best Effort: 當瀏覽器空間不足,會開始執行淘汰機制。
    • Chromium/Firefox: 從最少使用的(LRU)開始自動清除,不會通知用戶。
    • IE 10+: 不會清除,但會停止寫入。
  • Persistent: 不會被自動清除。

LRU policy

當硬碟空間即將用完時,瀏覽器會依據 LRU policy (least recently used) 規則清除快取:

  1. 目前沒有使用的網頁 (tabs/apps)。
  2. 比對最後存取的時間。

Persistent Storage

為了避免資料被淘汰機制刪除,我們可以透過程式碼啟用保護:

1
2
3
4
5
// 啟用 Persistent Storage
if (navigator.storage && navigator.storage.persist) {
const isPersisted = await navigator.storage.persist();
console.log(`Persisted storage granted: ${isPersisted}`);
}

離線後備頁面介紹 (offline fallback page)

離線後備頁面提供用戶在網路不穩定的情況下,一個備援的顯示頁面。

在過去的網站大多由伺服器提供,斷線時原則上就是什麼都沒有。近幾年 SPA 觀念興起,前端掌握度提高,一個有名的例子就是 Chrome 在斷線狀態下的小恐龍遊戲。

圖片來源: https://web.dev/

離線後備頁面實作

最佳實作方式是透過 service worker 搭配 Cache Storage API

接下來示範一個簡單的情境:當用戶網路斷線時,自動開啟離線頁面,並提供重試按鈕,當網路恢復時自動切換回正常頁面。

Google 的這個範例包含連線頁面與 service worker:

  1. 連線正常的第一頁
  2. 連線正常的第二頁
  3. 離線後備頁面
  4. service worker: 偵測到斷線時會將第一頁或第二頁切換到離線後備頁面

Service worker 離線實作程式碼

  1. 宣告常數
1
2
const CACHE_NAME = "offline";
const OFFLINE_URL = "offline.html";
  1. install 事件
1
2
3
4
5
6
7
self.addEventListener("install", (event) => {
event.waitUntil((async () => {
const cache = await caches.open(CACHE_NAME);
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
})());
self.skipWaiting();
});
  1. fetch 事件:攔截導覽請求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
event.respondWith((async () => {
try {
const preloadResponse = await event.preloadResponse;
if (preloadResponse) return preloadResponse;

const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// 僅在拋出例外(通常是網路錯誤)時執行
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})());
}
});

離線頁面設計建議

因為是離線頁面,建議將所有資源都快取起來,最簡單的方式是將所有資源(CSS/JS)都以 inline 方式寫在 HTML 中。

若需實作更複雜的策略,建議使用 Workbox

1
2
3
4
5
6
7
8
import { registerRoute } from "workbox-routing";
import { CacheFirst } from "workbox-strategies";

// 快取影片資源範例
registerRoute(
({ url }) => url.pathname.endsWith(".mp4"),
new CacheFirst({ cacheName: "media-cache" })
);

FAQ:PWA 用戶端儲存常見問題

Q1:我該在什麼時候使用 IndexedDB 而不是 LocalStorage?

A:LocalStorage 是同步的且上限僅 5MB,適合存簡單的字串(如佈題顏色)。若需要儲存大量結構化資料(如商品清單、文章內容)且不希望阻塞主線程效能,IndexedDB 是非同步的且空間限制極小,是更佳選擇。

Q2:瀏覽器什麼時候會主動清除我的 Cache API 資料?

A:當裝置硬碟空間不足時,瀏覽器會依據 LRU (Least Recently Used) 規則清除「Best Effort」類型的資料。除非您透過 navigator.storage.persist() 請求保護,否則所有快取資料都可能在極端情況下被移除。

Q3:離線後備頁面 (Offline Fallback) 對 SEO 有幫助嗎?

A:有。雖然 SEO 主要看伺服器回傳,但良好的離線體驗能降低跳出率並增加使用者留存時間,這間接對網站權威度有正面影響。此外,符合 PWA 標準的網站通常在 Google 搜尋排名中有更好的權重。


掌握了用戶端儲存與離線後備機制,您的 Web App 將具備如同原生應用般的韌性。持續優化快取策略,為使用者打造無縫的瀏覽體驗!


喜歡這篇文章,請幫忙拍拍手喔 🤣