オフライン データ

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

ストレージ

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

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

サポートされているプラットフォームで Storage Manager API を使用して、すべてのデバイス ストレージを管理できます。Cache Storage API と IndexedDB は、PWA 用の永続ストレージへの非同期アクセスを提供し、メインスレッド、Web ワーカー、Service Worker からアクセスできます。どちらも、ネットワークが不安定または存在しない場合に 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');
     }
   }
  });
}

この例では、id プロパティをストアのインデックス キーに設定して cookbook データベース内に recipes というオブジェクト ストアを作成し、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;
}

Cookie を追加すると、そのレシピは他のレシピとともにデータベースに格納されます。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 のストレージの管理方法を理解しておくことが特に重要です。

ストレージ容量は、Cache Storage、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 で [アプリケーション] タブの [ストレージ] セクションを開くと、サイトの割り当てとストレージの使用量をユーザーごとに確認できます。

アプリケーションの 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 ブラウザのサポート

ウェブ ストレージ

対応ブラウザ

  • 4
  • 12
  • 3.5
  • 4

ソース

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

対応ブラウザ

  • 86
  • 86
  • 111
  • 15.2

ソース

ストレージ マネージャ

対応ブラウザ

  • 55
  • 79
  • 57
  • 15.2

ソース

リソース