命令型キャッシュ ガイド

Andrew Guan
Andrew Guan

ウェブサイトによっては、結果を通知されることなくサービス ワーカーと通信する必要がある場合があります。次に例を示します。

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

このような重要でないタスクをサービス ワーカーに委任すると、メインスレッドが解放され、ユーザー操作への応答など、より緊急性の高いタスクを適切に処理できるというメリットがあります。

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

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

本番環境のケース

1-800-Flowers.com は、postMessage() を介してサービス ワーカーで強制キャッシング(プリフェッチ)を実装し、カテゴリページのトップアイテムをプリフェッチして、商品の詳細ページへのその後のナビゲーションを高速化しました。

1-800 Flowers のロゴ。

プリフェッチするアイテムを決定する方法は次のとおりです。

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

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

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

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

Workbox の使用

Workbox を使用すると、workbox-window パッケージ(ウィンドウ コンテキストで実行することを目的とした一連のモジュール)を介して、サービス ワーカーにメッセージを簡単に送信できます。これらは、サービス ワーカーで実行される他の 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
  }
});

ブラウザ API の使用

Workbox ライブラリがニーズを満たしていない場合は、ブラウザ API を使用して、ウィンドウからサービス ワーカーへの通信を実装する方法をご覧ください。

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

ページは、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-800-Flowers.com がプリキャッシュを試行する場合、ページは Service Worker が成功したかどうかを認識する必要はありません。対応している場合は、ユーザーはより速いナビゲーションを利用できます。そうでない場合は、新しいページに移動する必要があります。もう少し時間がかかります。

簡単なプリフェッチの例

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

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

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

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

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

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

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

まず、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',
    ],
  },
});

サービス ワーカーで 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 の配列を反復処理し、各 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 ヘッダーがあります)。このような場合は、フェッチしたリソースをサービス ワーカー キャッシュに保存することで、この動作をオーバーライドできます。これにより、オフライン シナリオでファイルを提供できるという利点もあります。

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 などの状況に備えて、このコードの周囲に例外処理を追加できます。ただし、サービス ワーカーを使用してプリフェッチを行うメリットは、ページとメインスレッドに大きな影響を与えることなく失敗できることです。プリフェッチされたコンテンツの処理後に、より複雑なロジックを追加して、処理するデータとの柔軟性を高め、分離することもできます。扱う内容に制限はありません。

まとめ

この記事では、ページとサービス ワーカー間の一方向通信の一般的なユースケースである強制キャッシュについて説明しました。ここで説明する例は、このパターンを使用する 1 つの方法を示すことのみを目的としています。同じアプローチは、オフラインでの使用、ブックマークなどのために人気記事をオンデマンドでキャッシュに保存するなどの他のユースケースにも適用できます。

ページとサービス ワーカーの通信のその他のパターンについては、以下をご覧ください。

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