命令型キャッシュ ガイド

Andrew Guan
Andrew Guan

ウェブサイトによっては、結果を知らせることなく Service Worker と通信する必要がある場合があります。以下に例を挙げます。

  • ページは Service Worker にプリフェッチする URL のリストを送信します。これにより、ユーザーがリンクをクリックすると、ドキュメントやページのサブリソースがすでにキャッシュにあるため、以降のナビゲーションが大幅に高速化されます。
  • このページは、一連の人気記事を取得してキャッシュに保存し、オフラインで利用できるようにするよう Service Worker に要求します。

このような重要でないタスクを Service Worker に委任すると、メインスレッドを解放して、ユーザー操作に応答するなどの緊急性の高いタスクをより適切に処理できるという利点があります。

Service Worker にキャッシュするリソースをリクエストしているページの図。

このガイドでは、標準のブラウザ API と Workbox ライブラリを使用して、ページから Service Worker への一方向の通信手法を実装する方法について説明します。このようなユースケースを命令型キャッシュと呼びます。

本番環境のケース

1-800-Flowers.com は、postMessage() を介して Service Worker で命令型キャッシュ(プリフェッチ)を実装し、カテゴリページの上位のアイテムをプリフェッチして、その後の商品詳細ページへの移動を高速化しました。

1-800 個の花のロゴ。

複数の方法でプリフェッチするアイテムを決定しています。

  • ページの読み込み時に、Service Worker は上位 9 つのアイテムの JSON データを取得し、得られたレスポンス オブジェクトをキャッシュに追加するよう要求します。
  • 残りのアイテムについては mouseover イベントをリッスンし、ユーザーがアイテムの上にカーソルを移動すると、「オンデマンドで」リソースの取得をトリガーできます。

Cache API を使用して JSON レスポンスを保存します。

1-800 個の花のロゴ。
1-800Flowers.com の商品リスティング ページから JSON 形式の商品データをプリフェッチする。

ユーザーがアイテムをクリックすると、そのアイテムに関連付けられた JSON データを、ネットワークを経由することなくキャッシュから取得できるため、ナビゲーションが高速化されます。

Workbox の使用

Workbox を使用すると、workbox-window パッケージを介して Service Worker に簡単にメッセージを送信できます。Service Worker は、ウィンドウ コンテキストで実行することを目的とした一連のモジュールです。Service Worker で実行される他の Workbox パッケージを補完するものです。

ページを Service Worker と通信するには、まず、登録済みの Service Worker への Workbox オブジェクト参照を取得します。

const wb = new Workbox('/sw.js');
wb.register();

そうすることで、宣言型の方法でメッセージを直接送信できます。その際、登録の取得や有効化の確認、基盤となる通信 API についての検討などの手間がかかりません。

wb.messageSW({"type": "PREFETCH", "payload": {"urls": ["/data1.json", "data2.json"]}}); });

Service Worker は、これらのメッセージをリッスンするために message ハンドラを実装します。オプションでレスポンスを返すこともできますが、このような場合は必要ありません。

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PREFETCH') {
    // do something
  }
});

Browser API の使用

Workbox ライブラリではニーズを満たせない場合、ブラウザ API を使用してウィンドウと Service Worker の間の通信を実装する方法を示します。

postMessage API を使用すると、ページから Service Worker への一方向の通信メカニズムを確立できます。

このページは、Service Worker インターフェースで postMessage() を呼び出します。

navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
  payload: 'some data to perform the task',
});

Service Worker は、これらのメッセージをリッスンするために message ハンドラを実装します。

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === MSG_ID) {
    // do something
  }
});

{type : 'MSG_ID'} 属性は必ずしも必要というわけではありませんが、ページから Service Worker にさまざまな種類の命令を送信できるようにするための 1 つの方法です(「プリフェッチ」と「ストレージを消去」など)。Service Worker は、このフラグに基づいて異なる実行パスに分岐できます。

オペレーションが成功した場合、ユーザーはメリットを得ることができますが、そうでない場合、メインのユーザーフローは変更されません。たとえば、1-800-Flowers.com が事前キャッシュを試みる場合、そのページは Service Worker が成功したかどうかを認識する必要はありません。そうすれば、ナビゲーションが速くなります。そうでない場合は、新しいページに移動する必要があります。少し時間がかかります。

プリフェッチの簡単な例

命令型キャッシュの最も一般的な用途の 1 つにプリフェッチがあります。プリフェッチとは、ユーザーが移動する前に特定の URL のリソースを取得し、ナビゲーションを高速化するものです。

サイトでプリフェッチを実装する方法はいくつかあります。

ドキュメントや特定のアセット(JS、CSS など)のプリフェッチなど、比較的単純なプリフェッチのシナリオでは、これらの手法が最適です。

プリフェッチ リソース(JSON ファイルまたはページ)を解析して内部 URL を取得するなど、追加のロジックが必要な場合は、このタスクを完全に Service Worker に委任することをおすすめします。

このようなオペレーションを Service Worker に委任すると、次のようなメリットがあります。

  • フェッチと取得後の処理(後で導入します)という面倒な作業をセカンダリ スレッドにオフロードします。これにより、メインスレッドは解放され、ユーザー操作への応答など、より重要なタスクを処理できるようになります。
  • 複数のクライアント(タブなど)が共通の機能を再利用できるようにし、さらにメインスレッドをブロックせずに同時にサービスを呼び出すこともできます。

商品の詳細ページをプリフェッチする

まず、Service Worker インターフェースで postMessage() を使用し、キャッシュに保存する URL の配列を渡します。

navigator.serviceWorker.controller.postMessage({
  type: 'PREFETCH',
  payload: {
    urls: [
      'www.exmaple.com/apis/data_1.json',
      'www.exmaple.com/apis/data_2.json',
    ],
  },
});

Service Worker で message ハンドラを実装し、アクティブなタブから送信されたメッセージをインターセプトして処理します。

addEventListener('message', (event) => {
  let data = event.data;
  if (data && data.type === 'PREFETCH') {
    let urls = data.payload.urls;
    for (let i in urls) {
      fetchAsync(urls[i]);
    }
  }
});

上記のコードでは、fetchAsync() という小さなヘルパー関数を導入して、URL の配列を反復処理し、それぞれに対してフェッチ リクエストを発行しました。

async function fetchAsync(url) {
  // await response of fetch call
  let prefetched = await fetch(url);
  // (optionally) cache resources in the service worker storage
}

レスポンスが取得されたら、リソースのキャッシュ ヘッダーを信頼できます。ただし、商品の詳細ページと同様に、多くの場合、リソースはキャッシュに保存されません(つまり、no-cacheCache-control ヘッダーがあります)。このような場合は、取得したリソースを Service Worker のキャッシュに保存することで、この動作をオーバーライドできます。これには、オフラインのシナリオでファイルを提供できるという利点もあります。

JSON データの枠を超える

JSON データがサーバー エンドポイントから取得されると、多くの場合、この第 1 レベルのデータに関連付けられた画像やエンドポイント データなど、プリフェッチも価値のある他の URL が含まれます。

この例では、食料品のショッピング サイトの JSON データが食料品店の情報であるとします。

{
  "productName": "banana",
  "productPic": "https://cdn.example.com/product_images/banana.jpeg",
  "unitPrice": "1.99"
 }

fetchAsync() コードを変更して、商品のリストを反復処理し、それぞれのヒーロー画像をキャッシュに保存します。

async function fetchAsync(url, postProcess) {
  // await response of fetch call
  let prefetched = await fetch(url);

  //(optionally) cache resource in the service worker cache

  // carry out the post fetch process if supplied
  if (postProcess) {
    await postProcess(prefetched);
  }
}

async function postProcess(prefetched) {
  let productJson = await prefetched.json();
  if (productJson && productJson.product_pic) {
    fetchAsync(productJson.product_pic);
  }
}

404 などの状況では、このコードの周囲に例外処理を追加できます。しかし、Service Worker を使用してプリフェッチを行う利点は、ページやメインスレッドに大きな影響を与えることなく失敗する可能性があることです。また、プリフェッチされたコンテンツの後処理に、より複雑なロジックを使用して柔軟性を高め、処理するデータとを分離することもできます。扱う内容に制限はありません。

おわりに

この記事では、ページと Service Worker 間の一方向通信の一般的なユースケースである命令型キャッシュについて説明しました。ここで説明する例は、このパターンを使用する 1 つの方法を説明することのみを目的としており、同じアプローチを他のユースケースにも適用できます。たとえば、トップ記事をオンデマンドでキャッシュしてオフラインで利用できるようにする、ブックマークする、などです。

ページと Service Worker のその他の通信パターンについては、以下をご覧ください。

  • アップデートのブロードキャスト: Service Worker からページを呼び出して、重要なアップデートを通知します(ウェブアプリの新しいバージョンが利用可能になったなど)。
  • 双方向通信: タスクを Service Worker に委任し(大量のダウンロードなど)、進行状況をページに知らせます。