背景
私はサービスワーカーは初めてですが、「オフラインファースト」になることを目的とした library に取り組んでいます(実際には、ほぼ「オフラインのみ」)(FWIW、その目的は、表形式のマルチリニアテキストを表すJSON設定を提供し、ユーザーが段落/バースの範囲で高度にカスタマイズ可能な方法でこれらのテキストを閲覧できるようにするアプリを取得するライブラリ。)
他のプロジェクトでは、ライブラリを依存関係としてインストールし、JavaScript APIを介して、アプリが(オフライン)アプリを作成するために使用するファイルを示すJSON構成ファイルのパスなどの情報を提供します。
私は次のいずれかを実行できることを知っていますが、
install
スクリプトが独自のJSONリクエストでwaitUntil
を使用してユーザーの必要なファイルを取得するためのハードコードされたパスをユーザーに提供することをユーザーに要求するinstall
ステップをスキップし、fetch
イベントを使用してキャッシュを更新します。ユーザーがインストールを完了し、フェッチが発生する前にオフラインになった場合のフォールバック表示を提供します。install
イベントが完了する前にクエリを実行するサーバーに、メインスクリプトからいくつかの状態情報を投稿します。...しかし、すべての選択は理想的とは言えない
install
イベントの後でオンラインにして、必要なフェッチがすべて発生することを確認します。質問:
メッセージまたは状態情報をService Workerに渡す方法はありますかbefore Service_Worker URLのクエリ文字列の一部として、またはメッセージングイベントを通じてinstall
イベントが発生しますか?メッセージングイベントは、install
内のwaitUntil
が完了する前に発生する可能性がある限り、install
イベントが開始した後でも技術的に到着する可能性があります。
これを自分でテストできることはわかっていますが、重要なアプリファイル自体を私たちのライブラリなどのように動的に取得する必要がある場合は、とにかくベストプラクティスを教えてください。
ここで、indexedDB
が唯一の代替手段になると思います(つまり、構成情報またはJSON構成のパスをindexedDBに保存し、Service Workerを登録し、install
イベント内からindexedDBデータを取得します)?ユーザーにストレージの名前空間を定義させているため、これでも理想的ではありませんが、ワーカーにも名前空間を渡す方法が必要です。そうしないと、Originの複数のアプリが競合する可能性があります。
便利であるとわかった場合は、はい。サービスワーカーのインストール時に、登録時に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から読み取ることです。
更新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]);
// });
});