クロスオリジン Service Worker - 外部フェッチの実験

背景

Service Worker を使用すると、ウェブ デベロッパーはウェブアプリが送信するネットワーク リクエストに応答できるため、オフラインでも作業を継続したり、lie-fi に対処したり、stale-while-revalidate などの複雑なキャッシュ操作を実装したりできます。ただし、Service Worker はこれまで特定のオリジンに関連付けられていました。ウェブアプリのオーナーは、ウェブアプリが行うすべてのネットワーク リクエストをインターセプトする Service Worker を作成してデプロイする責任があります。このモデルでは、各サービス ワーカーが、サードパーティ API やウェブフォントなど、クロスオリジン リクエストも処理します。

API、ウェブフォント、その他の一般的なサービスのサードパーティ プロバイダが、他のオリジンからオリジンへのリクエストを処理する独自のサービス ワーカーをデプロイできるとしたらどうでしょうか。プロバイダは独自のカスタム ネットワーキング ロジックを実装し、信頼できる単一のキャッシュ インスタンスを使用してレスポンスを保存できます。外部フェッチのおかげで、このようなサードパーティの Service Worker のデプロイが可能になりました。

外部フェッチを実装するサービス ワーカーをデプロイすることは、ブラウザからの HTTPS リクエストを介してアクセスされるサービスのプロバイダにとって理にかなっています。ブラウザが共通のリソース キャッシュを利用できる、ネットワークに依存しないバージョンのサービスを提供できるシナリオについて考えてみましょう。この機能のメリットを享受できるサービスには、次のようなものがあります(ただしこれらに限定されません)。

  • RESTful インターフェースを備えた API プロバイダ
  • ウェブフォント プロバイダ
  • アナリティクス プロバイダ
  • 画像ホスティング プロバイダ
  • 一般的なコンテンツ配信ネットワーク

たとえば、アナリティクス プロバイダの場合、外部取得 Service Worker をデプロイすると、ユーザーがオフラインのときに失敗したサービスへのすべてのリクエストがキューに追加され、接続が復元されたときに再実行されます。サービス クライアントはファースト パーティ サービス ワーカーを介して同様の動作を実装できましたが、サービスごとにカスタム ロジックを記述する必要があるため、デプロイした共有の外部フェッチ サービス ワーカーを使用するほどスケーラブルではありません。

前提条件

オリジン トライアル トークン

外部取得はまだ試験運用版です。ブラウザ ベンダーが完全に仕様を定めて合意する前に、この設計を早急に実装しないようにするため、この設計は Chrome 54 にオリジン トライアルとして実装されています。外部取得が試験運用版である限り、ホストするサービスでこの新機能を使用するには、サービスの特定の送信元にスコープが設定されたトークンをリクエストする必要があります。トークンは、外部取得で処理するリソースのすべてのクロスオリジン リクエストと、サービス ワーカーの JavaScript リソースのレスポンスに HTTP レスポンス ヘッダーとして含める必要があります。

Origin-Trial: token_obtained_from_signup

トライアルは 2017 年 3 月に終了します。その時点では、この機能を安定させるために必要な変更をすべて把握し、デフォルトで有効にすることを期待しています。期限までに外部取得がデフォルトで有効になっていない場合、既存のオリジン トライアル トークンに関連付けられた機能は動作を停止します。

公式のオリジン トライアル トークンを登録する前に、外部取得をテストできるようにするには、chrome://flags/#enable-experimental-web-platform-features に移動して「試験運用版のウェブ プラットフォームの機能」フラグを有効にすることで、ローカル PC の Chrome の要件を回避できます。ローカル テストで使用するすべての Chrome インスタンスでこの操作を行う必要があります。一方、オリジン トライアル トークンを使用すると、すべての Chrome ユーザーがこの機能を利用できます。

HTTPS

すべての Service Worker のデプロイと同様に、リソースと Service Worker スクリプトの両方の提供に使用するウェブサーバーは、HTTPS 経由でアクセスする必要があります。また、外部取得のインターセプションは、安全なオリジンでホストされているページから発信されたリクエストにのみ適用されるため、サービスのクライアントは HTTPS を使用して外部取得の実装を利用する必要があります。

外部取得の使用

前提条件を満たしたら、外部取得サービス ワーカーを稼働させるために必要な技術的な詳細について説明します。

Service Worker の登録

最初に直面する課題は、Service Worker を登録する方法です。Service Worker を扱ったことがある場合は、次のような点に慣れているでしょう。

// You can't do this!
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

ファーストパーティの Service Worker 登録用のこの JavaScript コードは、ユーザーが管理する URL にユーザーが移動したときにトリガーされるウェブアプリのコンテキストで意味があります。ただし、ブラウザがサーバーとやり取りするのは、完全なナビゲーションではなく、特定のサブリソースをリクエストする場合のみである場合、サードパーティのサービス ワーカーを登録することは現実的ではありません。ブラウザが、管理している CDN サーバーから画像をリクエストした場合、その JavaScript のスニペットをレスポンスに追加して実行することはできません。通常の JavaScript 実行コンテキスト外で、サービス ワーカーを登録する別の方法が必要です。

解決策は、サーバーが任意のレスポンスに含めることができる HTTP ヘッダーの形で提供されます。

Link: </service-worker.js>; rel="serviceworker"; scope="/"

上記のヘッダー例を、各コンポーネントが ; 文字で区切られているように分解してみましょう。

  • </service-worker.js> は必須で、サービス ワーカー ファイルのパスを指定するために使用されます(/service-worker.js は、スクリプトの適切なパスに置き換えます)。これは、navigator.serviceWorker.register() に最初のパラメータとして渡される scriptURL 文字列に直接対応します。値は <> 文字で囲む必要があります(Link ヘッダー仕様で義務付けられています)。絶対 URL ではなく相対 URL を指定した場合、レスポンスの場所を基準とする相対 URL として解釈されます。
  • rel="serviceworker" も必須で、カスタマイズなしで含める必要があります。
  • scope=/ は省略可能なスコープ宣言です。navigator.serviceWorker.register() の 2 番目のパラメータとして渡すことができる options.scope 文字列と同等です。多くのユースケースでは、デフォルトのスコープを使用できます。必要だとわかっている場合を除き、このスコープは省略できます。Link ヘッダーの登録には、許可される最大スコープに関する同じ制限が適用されます。また、Service-Worker-Allowed ヘッダーを使用してこれらの制限を緩和することもできます。

「従来型」のサービス ワーカーの登録と同様に、Link ヘッダーを使用すると、登録されたスコープに対して行われた次回のリクエストに使用されるサービス ワーカーがインストールされます。特別なヘッダーを含むレスポンスの本文はそのまま使用され、外部サービス ワーカーのインストールが完了するのを待たずに、ページですぐに使用できます。

なお、現在、外部取得は送信元トライアルとして実装されているため、Link レスポンス ヘッダーとともに、有効な Origin-Trial ヘッダーも含める必要があります。外部取得 Service Worker を登録するために追加する必要があるレスポンス ヘッダーの最小セットは次のとおりです。

Link: </service-worker.js>; rel="serviceworker"
Origin-Trial: token_obtained_from_signup

登録のデバッグ

開発中は、外部フェッチ サービス ワーカーが正しくインストールされ、リクエストを処理していることを確認する必要があります。Chrome のデベロッパー ツールで確認できる項目がいくつかあり、それらを確認することで、想定どおりに動作していることを確認できます。

適切なレスポンス ヘッダーが送信されているか

外部取得 Service Worker を登録するには、この投稿の前半で説明したように、ドメインでホストされているリソースへのレスポンスに Link ヘッダーを設定する必要があります。オリジン トライアル期間中、chrome://flags/#enable-experimental-web-platform-features が設定されていない場合は、Origin-Trial レスポンス ヘッダーも設定する必要があります。ウェブサーバーがこれらのヘッダーを設定していることを確認するには、DevTools の [ネットワーク パネル] のエントリを確認します。

[ネットワーク] パネルに表示されるヘッダー。

Foreign Fetch Service Worker が適切に登録されていますか?

また、DevTools の [Application パネル] で Service Worker の完全なリストを確認することで、そのスコープを含む基盤となる Service Worker の登録を確認することもできます。デフォルトでは、現在のオリジンの Service Worker のみが表示されるため、[すべて表示] オプションを選択してください。

[Applications] パネルの外部取得 Service Worker。

インストール イベント ハンドラ

サードパーティの Service Worker を登録したので、他の Service Worker と同様に、install イベントと activate イベントに応答できるようになります。これらのイベントを利用して、install イベント中に必要なリソースをキャッシュに格納したり、activate イベントで古いキャッシュを削除したりできます。

通常の install イベント キャッシュ アクティビティに加えて、サードパーティ サービス ワーカーの install イベント ハンドラ内で必要な追加の手順があります。コードで registerForeignFetch() を呼び出す必要があります。次の例をご覧ください。

self.addEventListener('install', event => {
    event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
    });
});

構成オプションは 2 つあり、どちらも必須です。

  • scopes は、1 つ以上の文字列の配列を受け取ります。各文字列は、foreignfetch イベントをトリガーするリクエストのスコープを指定します。でも待ってくださいService Worker の登録時にスコープを定義済みです、と思われるかもしれません。確かに、全体的なスコープは引き続き関連しています。ここで指定する各スコープは、Service Worker の全体的なスコープと等しいか、そのサブスコープである必要があります。ここに追加されたスコープ制限により、ファーストパーティの fetch イベント(自サイトからのリクエスト用)とサードパーティの foreignfetch イベント(他のドメインからのリクエスト用)の両方を処理できる汎用サービス ワーカーをデプロイし、より広いスコープのサブセットのみが foreignfetch をトリガーすることを明確にできます。実際には、サードパーティの foreignfetch イベントのみを処理する専用の Service Worker をデプロイする場合は、Service Worker の全体的なスコープと同じ単一の明示的なスコープを使用するだけです。上記の例では、値 self.registration.scope を使用してこの処理を行います。
  • origins は 1 つ以上の文字列の配列も受け取り、特定のドメインからのリクエストにのみ応答するように foreignfetch ハンドラを制限できます。たとえば、明示的に「https://example.com」を許可すると、https://example.com/path/to/page.html でホストされているページから、外部取得スコープから提供されるリソースに対してリクエストが送信されると、外部取得ハンドラがトリガーされますが、https://random-domain.com/path/to/page.html から送信されたリクエストはハンドラをトリガーしません。リモート送信元のサブセットに対してのみ外部取得ロジックをトリガーする特別な理由がない限り、配列に '*' のみを指定すると、すべての送信元が許可されます。

foreignfetch イベント ハンドラ

サードパーティの Service Worker をインストールし、registerForeignFetch() で構成したので、サーバーへのクロスオリジンのサブリソース リクエストをインターセプトできるようになりました。このリクエストは、外部取得スコープ内にあります。

従来のファーストパーティ Service Worker では、各リクエストで fetch イベントがトリガーされ、Service Worker がそのイベントに応答する機会がありました。Google のサードパーティ サービス ワーカーは、foreignfetch という少し異なるイベントを処理できます。概念的には、この 2 つのイベントは非常に似ています。どちらも、受信したリクエストを検査し、必要に応じて respondWith() を介してレスポンスを提供できます。

self.addEventListener('foreignfetch', event => {
    // Assume that requestLogic() is a custom function that takes
    // a Request and returns a Promise which resolves with a Response.
    event.respondWith(
    requestLogic(event.request).then(response => {
        return {
        response: response,
        // Omit to origin to return an opaque response.
        // With this set, the client will receive a CORS response.
        origin: event.origin,
        // Omit headers unless you need additional header filtering.
        // With this set, only Content-Type will be exposed.
        headers: ['Content-Type']
        };
    })
    );
});

コンセプトは類似していますが、ForeignFetchEventrespondWith() を呼び出す場合、実際にはいくつかの違いがあります。FetchEvent の場合のように、respondWith()Response(または Response で解決する Promise)を渡すのではなく、特定のプロパティを持つオブジェクトで解決する PromiseForeignFetchEventrespondWith() に渡す必要があります。

  • response は必須で、リクエストを行ったクライアントに返される Response オブジェクトに設定する必要があります。有効な Response 以外を指定すると、クライアントのリクエストはネットワーク エラーで終了します。fetch イベント ハンドラ内で respondWith() を呼び出す場合とは異なり、ここでは Response で解決される Promise ではなく、Response を指定する必要があります。Promise チェーンを使用してレスポンスを作成し、そのチェーンをパラメータとして foreignfetchrespondWith() に渡すことはできますが、チェーンは Response オブジェクトに設定された response プロパティを含むオブジェクトで解決する必要があります。このデモは、上記のコードサンプルで確認できます。
  • origin は省略可能です。返されるレスポンスが不透明かどうかを判断するために使用されます。これを省略すると、レスポンスは不透明になり、クライアントはレスポンスの本文とヘッダーに限定的にアクセスできるようになります。リクエストが mode: 'cors' で行われた場合、不透明なレスポンスを返すとエラーとして扱われます。ただし、リモート クライアントのオリジン(event.origin で取得可能)と同じ文字列値を指定すると、CORS 対応のレスポンスをクライアントに提供することを明示的にオプトインします。
  • headers も省略可能です。origin も指定して CORS レスポンスを返す場合にのみ役立ちます。デフォルトでは、CORS 許可リストの登録対象であるレスポンス ヘッダーのリストに登録されているヘッダーのみがレスポンスに含まれます。返される内容をさらにフィルタする必要がある場合は、headers に 1 つ以上のヘッダー名のリストを渡します。このリストは、レスポンスで公開するヘッダーの許可リストとして使用されます。これにより、機密性の高いレスポンス ヘッダーがリモート クライアントに直接公開されないようにしながら、CORS を有効にできます。

foreignfetch ハンドラが実行されると、Service Worker をホストしているオリジンのすべての認証情報とアンビエント権限にアクセスできることに注意してください。外部取得対応のサービス ワーカーをデプロイするデベロッパーは、それらの認証情報によって利用できない特権応答データを漏洩させないようにする責任があります。CORS レスポンスのオプトインを必須にすることは、意図しない漏洩を制限する 1 つの方法ですが、デベロッパーは、次の方法で、foreignfetch ハンドラ内で fetch() リクエストを明示的に実行し、暗黙的な認証情報を使用しないことができます。

self.addEventListener('foreignfetch', event => {
    // The new Request will have credentials omitted by default.
    const noCredentialsRequest = new Request(event.request.url);
    event.respondWith(
    // Replace with your own request logic as appropriate.
    fetch(noCredentialsRequest)
        .catch(() => caches.match(noCredentialsRequest))
        .then(response => ({response}))
    );
});

クライアントの検討事項

外部フェッチ サービス ワーカーがサービスのクライアントからのリクエストを処理する方法には、考慮すべき点がいくつかあります。

独自のファーストパーティ サービス ワーカーを持つクライアント

サービスの一部のクライアントには、ウェブアプリから発生したリクエストを処理する独自のファーストパーティ Service Worker がすでにある場合があります。これは、サードパーティの外部フェッチ Service Worker にとってどのような意味がありますか?

ファースト パーティ サービス ワーカーの fetch ハンドラは、リクエストをカバーするスコープで foreignfetch が有効になっているサードパーティ サービス ワーカーが存在する場合でも、ウェブアプリによって行われたすべてのリクエストに最初に応答する機会を得ます。ただし、ファーストパーティのサービス ワーカーを使用するクライアントは、引き続き外部フェッチ サービス ワーカーを利用できます。

ファースト パーティ Service Worker 内で fetch() を使用してクロスオリジン リソースを取得すると、適切な外部フェッチ Service Worker がトリガーされます。つまり、次のようなコードで foreignfetch ハンドラを利用できます。

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    // If event.request is under your foreign fetch service worker's
    // scope, this will trigger your foreignfetch handler.
    event.respondWith(fetch(event.request));
});

同様に、ファーストパーティの取得ハンドラが存在し、クロスオリジン リソースのリクエストを処理するときに event.respondWith() を呼び出さない場合、リクエストは自動的に foreignfetch ハンドラに「フォールスルー」します。

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    if (event.request.mode === 'same-origin') {
    event.respondWith(localRequestLogic(event.request));
    }

    // Since event.respondWith() isn't called for cross-origin requests,
    // any foreignfetch handlers scoped to the request will get a chance
    // to provide a response.
});

ファーストパーティの fetch ハンドラが event.respondWith() を呼び出すものの、fetch() を使用して外部取得スコープ内のリソースをリクエストしない場合、外部取得 Service Worker はリクエストを処理できません。

独自のサービス ワーカーを持たないクライアント

サードパーティ サービスにリクエストを送信するすべてのクライアントは、サービスが外部取得サービス ワーカーをデプロイすると、独自のサービス ワーカーを使用していなくてもメリットを得ることができます。外部フェッチ サービス ワーカーの使用を有効にするためにクライアントが特別に行う必要のあることはありません(ただし、外部フェッチ サービス ワーカーをサポートするブラウザを使用している必要があります)。つまり、外部取得サービス ワーカーをデプロイすると、カスタム リクエスト ロジックと共有キャッシュが、サービス クライアントの多くにすぐにメリットをもたらします。

まとめ: クライアントがレスポンスを求める場所

上記の情報を考慮すると、クライアントがクロスオリジン リクエストのレスポンスを検索するために使用するソースの階層を組み立てることができます。

  1. ファーストパーティ Service Worker の fetch ハンドラ(存在する場合)
  2. サードパーティ サービス ワーカーの foreignfetch ハンドラ(存在する場合、クロスオリジン リクエストの場合のみ)
  3. ブラウザの HTTP キャッシュ(新しいレスポンスが存在する場合)
  4. ネットワーク

ブラウザは上から順に開始し、Service Worker の実装に応じて、レスポンスのソースが見つかるまでリストを下っていきます。

その他の情報

最新情報の入手

Chrome での実装されている外部取得オリジン トライアルは、デベロッパーからのフィードバックに基づいて変更される可能性があります。本投稿は、インライン変更によって最新の状態に保たれます。具体的な変更については、変更の都度、以下に記載します。大きな変更については、@chromiumdev Twitter アカウントでもお知らせします。