オフライン データ

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

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

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

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

Cache Storage API は、HTML、CSS、JavaScript、画像、動画、音声など、URL を介してアクセスしたいネットワーク リソースや対象の 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 という別のインデックスを作成します。

作成したばかりのオブジェクト ストアを見てみましょう。オブジェクト ストアにレシピを追加して、DevTools(Chromium ベースのブラウザの場合)または Web Inspector(Safari の場合)を開くと、次のように表示されます。

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 が自動的に設定され、indexesdDB によって増加します。このコードを 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、Web Storage、さらには Service Worker ファイルとその依存関係を含むすべてのストレージ オプション間で共有されます。 ただし、使用できるストレージ容量はブラウザによって異なります。空き容量がなくなる可能性は低いです。ブラウザによっては、メガバイト、場合によってはギガバイト単位のデータが保存されることがあります。たとえば 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] の [Clear Storage] セクションにある Chrome DevTools

Firefox と Safari には、現在のオリジンのすべての保存容量と使用量を確認できる要約画面はありません。

データの永続化

非アクティブな状態やストレージの負荷が高まった場合のデータの自動エビクションを回避するために、互換性のあるプラットフォーム上の永続ストレージをブラウザにリクエストできます。許可すると、ブラウザはストレージからデータを削除しません。この保護の対象には、Service Worker の登録、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 の基準の一つは、たとえば PWA のインストールです。ユーザーがオペレーティング システムに PWA のアイコンをインストールしている場合、ブラウザが永続ストレージを許可する場合があります。

ストレージの永続化許可をユーザーに要求する Mozilla Firefox

で確認できます。

API ブラウザのサポート

ウェブ ストレージ

対応ブラウザ

  • Chrome: 4. <ph type="x-smartling-placeholder">
  • Edge: 12。 <ph type="x-smartling-placeholder">
  • Firefox: 3.5。 <ph type="x-smartling-placeholder">
  • Safari: 4. <ph type="x-smartling-placeholder">

ソース

ファイルシステムへのアクセス

対応ブラウザ

  • Chrome: 86。 <ph type="x-smartling-placeholder">
  • Edge: 86。 <ph type="x-smartling-placeholder">
  • Firefox: 111。 <ph type="x-smartling-placeholder">
  • Safari: 15.2。 <ph type="x-smartling-placeholder">

ソース

ストレージ マネージャ

対応ブラウザ

  • Chrome: 55。 <ph type="x-smartling-placeholder">
  • Edge: 79。 <ph type="x-smartling-placeholder">
  • Firefox: 57。 <ph type="x-smartling-placeholder">
  • Safari: 15.2。 <ph type="x-smartling-placeholder">

ソース

リソース