オフライン データ

堅牢なオフライン エクスペリエンスを構築するには、PWA にストレージ管理が必要です。キャッシュ保存の章では、キャッシュ ストレージはデバイスにデータを保存する方法の 1 つであることを学びました。この章では、データの永続性、上限、利用可能なツールなど、オフライン データを管理する方法について説明します。

ストレージ

ストレージには、ファイルやアセットだけでなく、他の種類のデータも含めることができます。PWA をサポートするすべてのブラウザでは、オンデバイス ストレージに次の API を使用できます。

  • IndexedDB: 構造化データと blob(バイナリデータ)用の NoSQL オブジェクト ストレージ オプション。
  • WebStorage: ローカル ストレージまたはセッション ストレージを使用して、Key-Value 文字列ペアを保存する方法。サービス ワーカーのコンテキスト内では使用できません。この API は同期型であるため、複雑なデータ ストレージには推奨されません。
  • キャッシュ ストレージ: キャッシュ モジュールで説明されているように。

サポートされているプラットフォームでは、Storage Manager API を使用してすべてのデバイス ストレージを管理できます。Cache Storage API と IndexedDB は、PWA の永続ストレージへの非同期アクセスを提供します。メインスレッド、ウェブワーカー、サービス ワーカーからアクセスできます。どちらも、ネットワークが不安定な場合や存在しない場合でも PWA を安定して動作させるうえで重要な役割を果たします。では、どちらを使用するべきでしょうか。

ネットワーク リソース(HTML、CSS、JavaScript、画像、動画、音声など、URL 経由でリクエストしてアクセスするリソース)には Cache Storage API を使用します。

IndexedDB を使用して構造化データを保存します。これには、NoSQL のような方法で検索または結合する必要があるデータや、URL リクエストと必ずしも一致しないユーザー固有のデータなど、その他のデータが含まれます。IndexedDB は、全文検索用に設計されていません。

IndexedDB

IndexedDB を使用するには、まずデータベースを開きます。データベースが存在しない場合は、新しいデータベースが作成されます。IndexedDB は非同期 API ですが、Promise を返す代わりにコールバックを受け取ります。次の例では、Jake Archibald の idb ライブラリを使用します。これは、IndexedDB 用の小さな Promise ラッパーです。IndexedDB を使用するのにヘルパー ライブラリは必要ありませんが、Promise 構文を使用する場合は idb ライブラリを使用できます。

次の例では、料理のレシピを保持するデータベースを作成します。

データベースの作成と開く

データベースを開くには:

  1. openDB 関数を使用して、cookbook という新しい IndexedDB データベースを作成します。IndexedDB データベースはバージョニングされているため、データベース構造を変更するたびにバージョン番号を増やす必要があります。2 つ目のパラメータはデータベースのバージョンです。この例では 1 に設定されています。
  2. upgrade() コールバックを含む初期化オブジェクトが openDB() に渡されます。コールバック関数は、データベースが初めてインストールされたとき、または新しいバージョンにアップグレードされたときに呼び出されます。アクションが発生するのはこの関数のみです。アクションには、新しいオブジェクト ストア(IndexedDB がデータの整理に使用する構造)やインデックス(検索するインデックス)の作成などがあります。また、データ移行もここで行う必要があります。通常、upgrade() 関数には break ステートメントのない switch ステートメントが含まれており、古いバージョンのデータベースに基づいて各ステップが順番に実行されます。
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');
     }
   }
  });
}

この例では、cookbook データベース内に recipes というオブジェクト ストアを作成し、id プロパティをストアのインデックスキーとして設定します。また、type プロパティに基づいて type という別のインデックスを作成します。

作成したオブジェクト ストアを確認しましょう。オブジェクト ストアにレシピを追加し、Chromium ベースのブラウザで DevTools または Safari で Web Inspector を開くと、次のように表示されます。

IndexedDB コンテンツを表示している Safari と Chrome。

データの追加

IndexedDB はトランザクションを使用します。トランザクションはアクションをグループ化するため、1 つの単位として実行されます。これらは、データベースが常に整合性のある状態であることを保証するのに役立ちます。また、アプリのコピーが複数実行されている場合は、同じデータへの同時書き込みを防ぐために重要です。データを追加するには:

  1. modereadwrite に設定してトランザクションを開始します。
  2. データを追加するオブジェクト ストアを取得します。
  3. 保存するデータを使用して add() を呼び出します。このメソッドは、辞書形式(Key-Value ペア)でデータを受け取り、オブジェクト ストアに追加します。辞書は、構造化クローニングを使用してクローンを作成できる必要があります。既存のオブジェクトを更新する場合は、代わりに put() メソッドを呼び出します。

トランザクションには done Promise があり、トランザクションが正常に完了すると解決されます。または、トランザクション エラーで拒否されます。

IDB ライブラリのドキュメントに説明されているように、データベースに書き込む場合、tx.done はすべてがデータベースに正常に commit されたことを示すシグナルです。ただし、個々のオペレーションを待機すると、トランザクションの失敗の原因となるエラーを確認できます。

// 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;
}

クッキーを追加すると、そのレシピは他のレシピとともにデータベースに保存されます。ID は indexedDB によって自動的に設定され、インクリメントされます。このコードを 2 回実行すると、同じ Cookie エントリが 2 つ作成されます。

データを取得しています

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 のストレージを管理する方法を知ることが特に重要です。

ストレージ容量は、キャッシュ ストレージ、IndexedDB、ウェブ ストレージ、さらにはサービス ワーカー ファイルとその依存関係など、すべてのストレージ オプションで共有されます。ただし、使用できるストレージの量はブラウザによって異なります。ただし、サイトによっては、ブラウザに数メガバイト、場合によっては数ギガバイトのデータが保存されるため、容量が不足することはほとんどありません。たとえば Chrome では、ブラウザが合計ディスク容量の最大 80% を使用できます。また、個々のオリジンはディスク容量全体の最大 60% を使用できます。Storage API をサポートしているブラウザでは、アプリで使用可能なストレージの残量、割り当て、使用量を確認できます。次の例では、Storage API を使用して割り当てと使用量の見積もりを取得し、使用率と残りのバイト数を計算します。navigator.storageStorageManager のインスタンスを返します。別個の 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 DevTools では、[Application] タブの [Storage] セクションを開くと、サイトの割り当てと、ストレージの使用状況(使用しているもの別)を確認できます。

Chrome DevTools の [Application]、[Clear Storage] セクション

Firefox と Safari には、現在のオリジンのすべてのストレージ割り当てと使用量を確認するための概要画面がありません。

データの永続化

対応プラットフォームでブラウザに永続ストレージをリクエストすると、非アクティブ状態やストレージの圧迫後にデータが自動的に強制排除されるのを回避できます。許可された場合、ブラウザはストレージからデータを強制的に削除することはありません。この保護には、サービス ワーカーの登録、IndexedDB データベース、キャッシュ ストレージ内のファイルが含まれます。なお、ユーザーは常に管理者であり、ブラウザが永続ストレージを許可している場合でも、いつでもストレージを削除できます。

永続ストレージをリクエストするには、StorageManager.persist() を呼び出します。これまでどおり、StorageManager インターフェースには navigator.storage プロパティを介してアクセスします。

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 の条件の 1 つは、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

リソース