オフライン データ

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

ストレージ

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

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

サポートされているプラットフォームでは、ストレージ マネージャー 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 インスペクタを開くと、次のような結果が表示されます。

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

データの追加

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

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

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

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

クッキーを追加すると、レシピは他のレシピとともにデータベースに保存されます。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 の [アプリケーション]、[ストレージを消去] セクション

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

リソース