離線資料

如要打造完善的離線體驗,您的 PWA 需要儲存空間管理功能。在快取章節中,您瞭解快取儲存空間是儲存裝置資料的一種方式。本章將說明如何管理離線資料,包括資料持續性、限制和可用的工具。

儲存空間

儲存空間不只用於檔案和素材資源,也能用於其他類型的資料。在所有支援 PWA 的瀏覽器中,下列 API 可用於裝置端儲存空間:

  • IndexedDB:適用於結構化資料和 Blob (二進位資料) 的 NoSQL 物件儲存空間選項。
  • WebStorage:使用本機或工作階段儲存空間儲存鍵/值字串組合的方式。但無法在服務工作者情境中使用。這個 API 是同步的,因此不建議用於複雜的資料儲存空間。
  • 快取儲存空間:請參閱「快取模組」一節。

您可以在支援的平台上使用 Storage Manager API 管理所有裝置儲存空間。Cache Storage API 和 IndexedDB 可為 PWA 提供非同步存取永久性儲存空間的功能,並可透過主執行緒、Web 工作站和服務工作站存取。在網路不穩定或無法連線時,這兩項功能都扮演著重要的角色,讓 PWA 可靠地運作。但您應該何時使用每個功能?

使用 Cache Storage API 存取網路資源,也就是您透過網址存取的資源,例如 HTML、CSS、JavaScript、圖片、影片和音訊。

使用 IndexedDB 儲存結構化資料。這類資料包括需要以 NoSQL 類似方式進行搜尋或組合的資料,或是其他資料 (例如不一定與網址要求相符的使用者專屬資料)。請注意,IndexedDB 並非全文搜尋的設計用途。

IndexedDB

如要使用 IndexedDB,請先開啟資料庫。如果沒有資料庫,這項作業會建立新的資料庫。IndexedDB 是個非同步 API,但會採用回呼,而非傳回 Promise。以下範例使用 Jake Archibald 的 idb 程式庫,這是 IndexedDB 的微型 Promise 包裝函式。使用 IndexedDB 不需要輔助程式庫,但如果您想使用 Promise 語法,可以選擇使用 idb 程式庫。

以下範例會建立資料庫來保存食譜。

建立及開啟資料庫

如要開啟資料庫,請按照下列步驟操作:

  1. 使用 openDB 函式建立名為 cookbook 的新 IndexedDB 資料庫。由於 IndexedDB 資料庫有版本號碼,因此每次變更資料庫結構時,都必須增加版本號碼。第二個參數是資料庫版本。在本範例中,此值設為 1。
  2. 包含 upgrade() 回呼的初始化物件會傳遞至 openDB()。系統會在首次安裝資料庫或升級至新版本時呼叫回呼函式。只有這個函式可以執行動作。動作可能包括建立新的物件儲存庫 (IndexedDB 用來整理資料的結構),或索引 (您想搜尋的項目)。這也是資料遷移作業應執行的時間點。通常,upgrade() 函式會包含 switch 陳述式,但沒有 break 陳述式,以便根據舊版資料庫的內容,讓每個步驟依序發生。
import { openDB } from 'idb';

async function createDB() {
  // Using https://github.com/jakearchibald/idb
  const db = await openDB('cookbook', 1, {
    upgrade(db, oldVersion, newVersion, transaction) {
      // Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded.
    switch(oldVersion) {
     case 0:
       // Placeholder to execute when database is created (oldVersion is 0)
     case 1:
       // Create a store of objects
       const store = db.createObjectStore('recipes', {
         // The `id` property of the object will be the key, and be incremented automatically
           autoIncrement: true,
           keyPath: 'id'
       });
       // Create an index called `name` based on the `type` property of objects in the store
       store.createIndex('type', 'type');
     }
   }
  });
}

這個範例會在名為 recipescookbook 資料庫中建立物件儲存庫,並將 id 屬性設為儲存庫的索引鍵,然後根據 type 屬性建立另一個名為 type 的索引。

我們來看看剛剛建立的物件儲存庫。在物件儲存庫中新增食譜,並在以 Chromium 為基礎的瀏覽器中開啟 DevTools,或在 Safari 中開啟 Web Inspector 後,您應該會看到以下畫面:

Safari 和 Chrome 顯示 IndexedDB 內容。

新增資料

IndexedDB 會使用交易。交易會將動作分組,以便以單一單位進行。這有助確保資料庫一律處於一致狀態。如果您有多個正在執行的應用程式副本,這些值也非常重要,可避免同時寫入相同資料。如要新增資料,請按照下列步驟操作:

  1. mode 設為 readwrite,開始交易。
  2. 取得物件儲存庫,以便新增資料。
  3. 使用要儲存的資料呼叫 add()。該方法會以字典格式 (鍵/值組合) 接收資料,並將資料新增至物件儲存空間。字典必須可使用結構化複製功能進行複製。如果您想更新現有物件,請改為呼叫 put() 方法。

交易具有 done 承諾,在交易成功完成時會解析,或在發生交易錯誤時拒絕。

IDB 程式庫文件說明所述,如果您要寫入資料庫,tx.done 就是所有內容已成功儲存至資料庫的信號。不過,等待個別作業會有所助益,因為您可以查看導致交易失敗的任何錯誤。

// Using https://github.com/jakearchibald/idb
async function addData() {
  const cookies = {
      name: "Chocolate chips cookies",
      type: "dessert",
        cook_time_minutes: 25
  };
  const tx = await db.transaction('recipes', 'readwrite');
  const store = tx.objectStore('recipes');
  store.add(cookies);
  await tx.done;
}

加入 Cookie 後,這個食譜就會與其他食譜一同儲存在資料庫中。系統會自動設定並遞增 indexedDB 的 ID。如果您執行這段程式碼兩次,就會有兩個相同的 Cookie 項目。

正在擷取資料

以下說明如何從 IndexedDB 取得資料:

  1. 開始交易並指定物件儲存空間,以及選用的交易類型。
  2. 從該交易呼叫 objectStore()。請務必指定物件儲存庫名稱。
  3. 使用您要取得的鍵呼叫 get()。根據預設,儲存庫會使用其索引鍵。
// Using https://github.com/jakearchibald/idb
async function getData() {
  const tx = await db.transaction('recipes', 'readonly')
  const store = tx.objectStore('recipes');
// Because in our case the `id` is the key, we would
// have to know in advance the value of the id to
// retrieve the record
  const value = await store.get([id]);
}

儲存空間管理工具

如要正確儲存及串流網路回應,瞭解如何管理 PWA 儲存空間就顯得格外重要。

儲存空間容量會在所有儲存空間選項之間共用,包括 Cache Storage、IndexedDB、Web Storage,甚至是服務工作者檔案及其依附元件。不過,可用的儲存空間容量會因瀏覽器而異。您不太可能用盡這些空間,因為網站可以在部分瀏覽器上儲存數 MB 甚至數 GB 的資料。舉例來說,Chrome 允許瀏覽器使用最多 80% 的磁碟空間,而個別來源最多可使用 60% 的磁碟空間。如果瀏覽器支援 Storage API,您可以瞭解應用程式可用的儲存空間、配額和用量。以下範例使用 Storage API 取得預估配額和用量,然後計算已用百分比和剩餘位元組。請注意,navigator.storage 會傳回 StorageManager 的例項。Storage 介面是獨立的,很容易混淆。

if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

在 Chromium 開發人員工具中,您可以開啟「應用程式」分頁中的「儲存空間」部分,查看網站配額和儲存空間用量,並按用量細分。

Chrome 開發人員工具中的「應用程式」部分,清除儲存空間

Firefox 和 Safari 不提供摘要畫面,無法查看目前來源的所有儲存空間配額和用量。

資料持續性

您可以在相容平台上要求瀏覽器提供永久儲存空間,以免在閒置或儲存空間不足時,系統自動淘汰資料。如果授予權限,瀏覽器就不會從儲存空間中移除資料。這項保護機制涵蓋服務工作者註冊、IndexedDB 資料庫,以及快取儲存空間中的檔案。請注意,使用者隨時可以刪除儲存空間,即使瀏覽器已授予永久儲存空間,也一樣。

如要要求永久儲存空間,請呼叫 StorageManager.persist()。如同先前所述,您可以透過 navigator.storage 屬性存取 StorageManager 介面。

async function persistData() {
  if (navigator.storage && navigator.storage.persist) {
    const result = await navigator.storage.persist();
    console.log(`Data persisted: ${result}`);
}

您也可以呼叫 StorageManager.persisted(),檢查目前來源是否已授予永久性儲存空間。Firefox 會向使用者要求使用永久性儲存空間的權限。以 Chromium 為基礎的瀏覽器會根據啟發式搜尋決定內容對使用者的重要性,進而決定是否允許持續性。舉例來說,Google Chrome 的其中一個評估標準是 PWA 安裝作業。如果使用者已在作業系統中安裝 PWA 圖示,瀏覽器可能會授予永久性儲存空間。

Mozilla Firefox 要求使用者授予儲存空間持久性權限。

API 瀏覽器支援

網路儲存空間

Browser Support

  • Chrome: 4.
  • Edge: 12.
  • Firefox: 3.5.
  • Safari: 4.

Source

檔案系統存取權

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

儲存空間管理工具

Browser Support

  • Chrome: 55.
  • Edge: 79.
  • Firefox: 57.
  • Safari: 15.2.

Source

資源