web-dev-qa-db-ja.com

「インストール」の前に状態情報をService Workerに渡す

背景

私はサービスワーカーは初めてですが、「オフラインファースト」になることを目的とした library に取り組んでいます(実際には、ほぼ「オフラインのみ」)(FWIW、その目的は、表形式のマルチリニアテキストを表すJSON設定を提供し、ユーザーが段落/バースの範囲で高度にカスタマイズ可能な方法でこれらのテキストを閲覧できるようにするアプリを取得するライブラリ。)

他のプロジェクトでは、ライブラリを依存関係としてインストールし、JavaScript APIを介して、アプリが(オフライン)アプリを作成するために使用するファイルを示すJSON構成ファイルのパスなどの情報を提供します。

私は次のいずれかを実行できることを知っていますが、

  1. サービスワーカーのinstallスクリプトが独自のJSONリクエストでwaitUntilを使用してユーザーの必要なファイルを取得するためのハードコードされたパスをユーザーに提供することをユーザーに要求する
  2. jSONファイルのService WorkerのService Workerのinstallステップをスキップし、fetchイベントを使用してキャッシュを更新します。ユーザーがインストールを完了し、フェッチが発生する前にオフラインになった場合のフォールバック表示を提供します。
  3. サービスワーカーが登録すると、installイベントが完了する前にクエリを実行するサーバーに、メインスクリプトからいくつかの状態情報を投稿します。

...しかし、すべての選択は理想的とは言えない

  1. 私たちのライブラリのコンシューマは、JSON設定に独自の場所を指定できることを好む場合があります。
  2. JSON構成は、ユーザーに役立つものを表示するために重要なファイルを指定しているので、ユーザーがオンラインに戻らないと残りのファイルを取得できないとだけ言って、インストールを完了させたくないinstallイベントの後でオンラインにして、必要なフェッチがすべて発生することを確認します。
  3. サーバーや余分なコードへのさらなるトリップを避けたいだけでなく、コードをオフライン指向にして、単なる静的ファイルサーバーで完全に機能できるようにしたいと思います。

質問:

メッセージまたは状態情報をService Workerに渡す方法はありますかbefore Service_Worker URLのクエリ文字列の一部として、またはメッセージングイベントを通じてinstallイベントが発生しますか?メッセージングイベントは、install内のwaitUntilが完了する前に発生する可能性がある限り、installイベントが開始した後でも技術的に到着する可能性があります。

これを自分でテストできることはわかっていますが、重要なアプリファイル自体を私たちのライブラリなどのように動的に取得する必要がある場合は、とにかくベストプラクティスを教えてください。

ここで、indexedDBが唯一の代替手段になると思います(つまり、構成情報またはJSON構成のパスをindexedDBに保存し、Service Workerを登録し、installイベント内からindexedDBデータを取得します)?ユーザーにストレージの名前空間を定義させているため、これでも理想的ではありませんが、ワーカーにも名前空間を渡す方法が必要です。そうしないと、Originの複数のアプリが競合する可能性があります。

20
Brett Zamir

クエリパラメータの使用

便利であるとわかった場合は、はい。サービスワーカーのインストール時に、登録時にService Workerにクエリパラメーターを含めることで状態を提供できます。

_// Inside your main page:
const pathToJson = '/path/to/file.json';
const swUrl = '/sw.js?pathToJson=' + encodeURIComponent(pathToJson);
navigator.serviceWorker.register(swUrl);

// Inside your sw.js:
self.addEventListener('install', event => {
  const pathToJson = new URL(location).searchParams.get('pathToJson');
  event.waitUntil(
    fetch(pathToJson)
      .then(response => response.json())
      .then(jsonData => /* Do something with jsonData */)
  );
});
_

このアプローチについて注意すべき点がいくつかあります。

  • installハンドラーでJSONファイルをfetch()した場合(コードサンプルのように)、それはサービスワーカースクリプトのバージョンごとに1回(_sw.js_)効果的に発生します。 JSONファイルの内容が変更されても、その他はすべて同じである場合、Service Workerはそれを自動的に検出してキャッシュを再生成しません。

  • 最初の点から、JSONファイルのURLにハッシュベースのバージョン管理を含めるなどの方法で回避すると、そのURLを変更するたびに、新しいService Workerがインストールされます。これ自体は悪いことではありませんが、WebアプリにService Workerライフサイクルイベントをリッスンするロジックがある場合は、これを覚えておく必要があります。

代替アプローチ

また、Cache Storage APIをサポートするブラウザーは_window.caches_を介してファイルを公開するため、メインページのコンテキスト内からキャッシュにファイルを追加する方が簡単な場合もあります。ただし、Service Workerのinstallハンドラー内でファイルを事前キャッシュすると、Service Workerのインストール前にすべてのファイルが正常にキャッシュされるという利点があります。

別のアプローチは、windowコンテキストからIndexedDBに状態情報を書き込み、Service Workerのinstallハンドラー内のIndexedDBから読み取ることです。

32
Jeff Posnick

更新3:

また、ワーカー内のグローバルに依存するのは安全ではないため、私のメッセージングソリューションの音はさらに低くなります。私はそれがジェフ・ポズニックの解決策である必要があると思います(場合によっては、importScriptsがうまくいくかもしれません)。

更新2:

「インストール」イベントに関連するこのスレッドのトピックに直接関係はありませんが、 https://github.com/w3c/ServiceWorker/issues/659#issuecomment-38491905 で始まるディスカッションに従って、特にactivateイベントにこのメッセージパッシングアプローチを使用すると、いくつかの問題があります。つまり、activateイベントが失敗することはなく、再試行されることもないため、アプリケーションは不安定な状態のままになります。 (installの失敗は、少なくとも新しいサービスワーカーを古いページに適用しませんが、activateは、イベントが完了するまでフェッチを保留し続けます。受信されなかったメッセージ、および新しいページをロードしてそのメッセージを再度送信することができないため、新しいワーカー以外は修正に失敗します。)

更新:

Chromeのinstallスクリプトからクライアントを取得しましたが、何らかの理由でnavigator.serviceWorker.onmessageでメッセージを受信できませんでした。

しかし、代わりに次のアプローチを完全に確認することができました。

Service Workerの場合:

self.addEventListener('install', e => {
    e.waitUntil(
        new Promise((resolve, reject) => {
            self.addEventListener('message', ({data: {
                myData
            }}) => {
                // Do something with `myData` here
                //    then when ready, `resolve`
            });
        })
    );
 });

呼び出しスクリプト内:

navigator.serviceWorker.register('sw.js').then((r) => {
    r.installing.postMessage({myData: 100});
});

OPで説明した単純なケースでは@JeffPosnickが最善の答えですが、次のような方法で、メッセージをService Workerスクリプト(Chromeでテスト済み)との間で早期に取得できることを発見したと思います。 :

サービスワーカー内

self.addEventListener('install', e => {
    e.waitUntil(self.clients.matchAll({
        includeUncontrolled: true,
        type: 'window'
    }).then((clients) => new Promise((resolve, reject) => {
        if (clients && clients.length) {
            const client = clients.pop();
            client.postMessage('send msg to main script');
            // One should presumably be able to poll to check for a
            //   variable set in the SW message listener below
            //   and then `resolve` when set
            // Despite the unreliability of setting globals in SW's
            //   I believe this could be safe here as the `install`
            //   event is to run while the main script is still open.
        }
    })));
});

self.addEventListener('message', e => {
    console.log('SW receiving main script msg', e.data);
    e.ports[0].postMessage('sw response');
});

呼び出しスクリプト内:

navigator.serviceWorker.addEventListener('message', (e) => {
    console.log('msg recd in main script', e.data);
    e.source.postMessage('sending back to sw');
});
return navigator.serviceWorker.register(
    'sw.js'
).then((r) => {
    // navigator.serviceWorker.ready.then((r) => { // This had been necessary at some point in my testing (with r.active.postMessage), but not working for me atm...
        // Sending a subsequent message
        const messageChannel = new MessageChannel();
        messageChannel.port1.onmessage = (e) => {
            if (e.data.error) {
                console.log('err', e.data.error);
            } else {
                console.log('data', e.data);
            }
        };
        navigator.serviceWorker.controller.postMessage('sending to sw', [messageChannel.port2]);
    // });
});
2
Brett Zamir