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

背景

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

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

外部フェッチを実装する Service Worker をデプロイすることは、ブラウザから HTTPS リクエストを介してアクセスするサービス プロバイダにとって有益です。ネットワークに依存しないバージョンのサービスを提供し、ブラウザが共通のリソース キャッシュを利用できる状況について考えてみてください。メリットが得られるサービスとしては以下が挙げられますが、これらに限定されません。

  • RESTful インターフェースを持つ API プロバイダ
  • ウェブフォント プロバイダ
  • アナリティクス プロバイダ
  • 画像ホスティング プロバイダ
  • 汎用コンテンツ配信ネットワーク

たとえば、分析プロバイダであるとします。外部フェッチの Service Worker をデプロイすることで、ユーザーがオフラインである間に失敗したサービスに対するすべてのリクエストをキューに入れて、接続が回復したらリプレイできるようになります。サービスのクライアントがファーストパーティの Service Worker を介して同様の動作を実装することは可能でしたが、各クライアントにサービス専用のロジックを記述しなければならないのは、デプロイする外部フェッチの Service Worker にデプロイした場合ほどスケーラブルではありません。

前提条件

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

外部取得はまだ試験運用版と見なされます。ブラウザ ベンダーが完全に指定して合意するまで、この設計が時期尚早に組み込まれるのを防ぐため、Chrome 54 ではオリジン トライアルとして実装されています。外部取得が試験運用版である限り、ホストしているサービスでこの新機能を使用するには、サービス固有の生成元をスコープとするトークンをリクエストする必要があります。このトークンは、外部フェッチで処理するリソースのすべてのクロスオリジン リクエストの HTTP レスポンス ヘッダーと、Service Worker JavaScript リソースのレスポンスに含める必要があります。

Origin-Trial: token_obtained_from_signup

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

正式なオリジン トライアル トークンに登録する前に外部フェッチの試験運用を簡単にするため、chrome://flags/#enable-experimental-web-platform-features に移動して [試験運用版ウェブ プラットフォーム機能] フラグを有効にすると、ローカル コンピュータの Chrome での要件を回避できます。この設定は、ローカルテストで使用する Chrome のすべてのインスタンスで行う必要があります。オリジン トライアル トークンを使用すると、すべての Chrome ユーザーがこの機能を使用できるようになります。

HTTPS

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

外部フェッチの使用

前提条件がなくなったので、外部フェッチの Service Worker の稼働に必要な技術的な詳細を詳しく見てみましょう。

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 に移動するとトリガーされるウェブアプリのコンテキストで有効です。しかし、ブラウザがサーバーとのやり取りを行う唯一の手段として、完全なナビゲーションではなく特定のサブリソースを要求するような場合、サードパーティの Service Worker を登録する方法は実用的ではありません。ブラウザが、たとえば、自身で管理する CDN サーバーからの画像をリクエストした場合、その JavaScript のスニペットをレスポンスの先頭に追加して、実行されることを想定することはできません。通常の JavaScript 実行コンテキスト以外で、別の方法で Service Worker を登録する必要があります。

この問題を解決するには、サーバーが任意のレスポンスに含めることができる HTTP ヘッダーを使用します。

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

このヘッダーの例を複数のコンポーネントに分解してみましょう。各コンポーネントは ; 文字で区切られています。

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

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

外部取得は現在、オリジン トライアルとして実装されているため、リンク レスポンス ヘッダーに加えて、有効な 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 の [Network] パネルのエントリで確認できます。

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

外部フェッチ サービス ワーカーは正しく登録されていますか?

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

[Applications] パネルの外部フェッチ サービス ワーカー。

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

サードパーティの Service Worker を登録したら、他の Service Worker と同じように install イベントや activate イベントに応答できるようになります。これらのイベントを利用して、たとえば install イベント中にキャッシュに必要なリソースを取り込み、activate イベントで古くなったキャッシュをプルーニングできます。

通常の install イベント キャッシュ アクティビティに加えて、サードパーティの Service Worker の 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 イベント(他のドメインからのリクエストの場合)の両方を処理できる汎用 Service Worker をデプロイできます。また、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 から行われたリクエストではハンドラはトリガーされません。リモートオリジンのサブセットに対してのみ外部取得ロジックをトリガーする特別な理由がない限り、配列の唯一の値として '*' を指定するだけで、すべてのオリジンが許可されます。

externalfetch イベント ハンドラ

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

従来のファーストパーティの Service Worker では、リクエストごとに fetch イベントがトリガーされ、Service Worker はこれに応答する機会がありました。サードパーティの Service Worker には、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']
        };
    })
    );
});

概念的な類似点にもかかわらず、ForeignFetchEvent に対して respondWith() を呼び出す場合、実際にはいくつか違いがあります。FetchEvent の場合のように、単に Response(または Response で解決される Promise)を respondWith() に渡すのではなく、特定のプロパティを持つオブジェクトで解決される 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 をホストしている送信元のすべての認証情報とアンビエント オーソリティにアクセスできることに注意してください。外部でフェッチ対応の 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 を持つクライアント

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

ファーストパーティの Service Worker の fetch ハンドラは、リクエストを対象とするスコープで foreignfetch が有効なサードパーティの Service Worker があっても、ウェブアプリが行うすべてのリクエストに応答する最初の機会を得られます。ただし、ファースト パーティ Service Worker を使用するクライアントは、引き続き外部フェッチ 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 はリクエストを処理できません。

独自の Service Worker を持たないクライアント

サードパーティ サービスにリクエストを送信するすべてのクライアントは、独自の Service Worker を使用していない場合でも、サービスが外部フェッチ Service Worker をデプロイすることでメリットを得られます。外部フェッチの Service Worker をサポートするブラウザを使用している限り、外部フェッチの Service Worker の使用にオプトインするためにクライアントが実施しなければならない操作は特にありません。つまり、外部フェッチ Service Worker をデプロイすることで、カスタム リクエスト ロジックと共有キャッシュは、追加の手順を踏むことなく、サービスの多くのクライアントにとってすぐに恩恵を受けます。

すべてを組み合わせる: クライアントが応答を求める場所

上記の情報を考慮すると、クライアントがクロスオリジン リクエストのレスポンスを見つけるために使用するソースの階層を構築できます。

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

ブラウザは上から順に進み、Service Worker の実装に応じて、レスポンスのソースが見つかるまでリストを上に向かって続けます。

詳細

最新情報の入手

Chrome での外国取得オリジン トライアルの実装は、デベロッパーからのフィードバックに応じて変更される場合があります。この投稿ではインライン変更によって最新情報をお知らせします。具体的な変更点については、随時お知らせします。また、重要な変更に関する情報は、Twitter アカウント @chromiumdev でお知らせします。