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

背景

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

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

外部フェッチを実装する 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 ヘッダーを介してそれらの制限を緩和する機能が適用されます。

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

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

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

登録をデバッグする

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

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

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

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

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

DevTools のアプリケーション パネルで、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 イベントがトリガーされ、これに応答する機会がありました。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 で行うように、Response(または Response で解決される Promise)を respondWith() に提供する代わりに、特定のプロパティを持つオブジェクトで解決される PromiseForeignFetchEventrespondWith() に渡す必要があります。

  • response は必須で、リクエストを行ったクライアントに返される Response オブジェクトに設定する必要があります。有効な Response 以外を指定すると、クライアントのリクエストはネットワーク エラーで終了します。fetch イベント ハンドラ内で respondWith() を呼び出す場合とは異なり、ここでは Response で解決される Promise ではなく、Response を指定する必要があります。Promise チェーンを使用してレスポンスを作成し、そのチェーンをパラメータとして foreignfetchrespondWith() に渡すことができます。ただし、このチェーンは、Response オブジェクトに設定された response プロパティを含むオブジェクトで解決する必要があります。このデモは、上記のコードサンプルで確認できます。
  • origin は省略可能で、返されるレスポンスが opaque であるかどうかを判定するために使用されます。これを省略すると、レスポンスは不透明になり、クライアントはレスポンスの本文とヘッダーに限定的にアクセスできるようになります。リクエストが 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}))
    );
});

クライアントの検討事項

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

独自の Google Service Worker を使用するクライアント

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

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

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

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

その他の情報

最新情報の入手

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