概要
Service Worker とは、バックグラウンドで動く JavaScript。オフラインで動くウェブアプリを作るために便利なイベントが定義されている。今まで Application Cache 等の宣言的なキャッシュ方法が提案されて来たが、なかなか全てのユースケースをカバーするのは難しいので、柔軟なキャッシュを実装するための低レベル API を提案してみたという事らしい。なので、実際には https://workboxjs.org/ 等のライブラリ経由で使う方が楽。
Service Worker の動作には HTTPS サーバか localhost で動くウェブサーバが必要。開発には Web Server for Chrome が便利。他の人に見せたい場合は Github pages や Amazon Cloud Front を使えば良い。
登録と削除
登録には、navigator.serviceWorker にある ServiceWorkerContainer の register() で JavaScript ファイルを指定する。下の例では service-worker.js
を登録している。ただし、実際に有効になるのは次回アクセス時なので、ブラウザのリロードが必要。有効になった Service worker は、navigator.serviceWorker.controller から取得出来る。
// client side
navigator.serviceWorker.register("service-worker.js")
.then(success => console.log("Success: Service worker is registered.", success))
.catch(error => console.log("Error: Service worker is not registered.", error));
削除には navigator.serviceWorker.getRegistrations() にある ServiceWorkerRegistration の unregister() を使う。unregister() は ServiceWorkerRegistration のリストの Promise を返すのでコードは少しややこしい。こちらも削除してからブラウザをリロードするまでは元の Service worker は有効のままになっている。
// client side
navigator.serviceWorker.getRegistrations()
.then(registrations => {
for (let registration of registrations) {
registration.unregister();
}
})
Service worker でキャッシュを行う
典型的な使い方であるキャッシュを使って、Service worker の動作を調べる。
Service worker には ServiceWorkerGlobalScope を指す self がグローバルに定義されている。この self を使って色んな事をする。
キャッシュを保存する
Service worker のキャッシュとは、簡単に言うとファイル名がキーで Response が値の辞書だ。これは昔からブラウザに実装されているキャッシュとは別にプログラマが自分で管理するキャッシュデータベースなので、勝手に消えたりしない。
Service worker インストール時の処理は install イベントで行う。典型的にはここでキャッシュの保存を行う。この例では、'index.html' と 'offline.png' の二つのファイルをキャッシュしてみた。この処理は navigator.serviceWorker.register()
で登録した service-worker.js に書く。
// service worker side
self.addEventListener('install', event => {
event.waitUntil(
self.caches.open('my-cache')
.then(cache => cache.addAll(['index.html', 'offline.png']))
);
});
Service worker では、キャッシュのために CacheStorage と Cache という二種類のオブジェクトが用意されている。CacheStorage の中に複数の Cache を入れる事が出来るのでこれを使ってキャッシュのバージョン管理を行うらしい。
- self.caches を通じて CacheStorage にアクセスする。CacheStorage には元のコンテンツからでも Service worker からでもアクセス出来る。
- CacheStorage の open(cacheName) で使いたい Cache にアクセスする。この例では
my-cache
という cacheName を使った。非同期なのでややこしいが、CacheStorage は文字列をキーとする辞書で値は Cache だ。はたまた Cache は URL (文字列) をキーとした辞書で match で値の Response を取得する。 - waitUntil というのは、未実行の処理を残して Service worker が止まってしないようにする物らしい。
キャッシュを表示する
ここが非常に面白い所。Service worker は管理しているコンテンツのインターネットアクセスに割り込んで好きな内容に変えてしまう事が出来る。インターネットアクセスに割り込むには fetch イベントを使う。
// service worker side
self.addEventListener('fetch', event => {
if (event.request.url.endsWith('status.png')) {
// Intercept the request for 'status.png'
event.respondWith(
fetch('worker.png')
.catch(error => self.caches.match('offline.png'))
);
} else {
// Otherwise, return the cache.
event.respondWith(
self.caches.match(event.request)
.then(response => {
if (response)
return response;
console.log("Worker: No response", event.request);
return fetch(event.request);
})
);
}
});
ここではちょっと凝ったことをしている。
-
status.png
へのリクエストを見つけると、ネットワークが生きている時は代わりにworker.png
のリクエストに変えてしまう。 -
status.png
へのリクエストを見つけると、ネットワークが死んでいる時はキャッシュに保存しておいたoffline.png
のレスポンスを返す。 - その他のリクエストを見つけると、キャッシュに保存しておいたレスポンスを返す。
その他難しい部分。
- キャッシュから Response を取り出すには match を使う。Cache にも CacheStorage にも match は実装されていて、CacheStorage の match を使うと中の Cache 全てから検索する。結果は Promise なのでややこしい。
- event.respondWith を使って代わりのレスポンスをクライアントに返す
Message を使って Service worker と通信
クライアントと Service worker との通信には postMessage と message イベントを使う。以下にコード例を書く。
クライアントから Service worker にメッセージを送る。
ServiceWorker postMessage: クライアントから Service worker に送る。
// client side
navigator.serviceWorker.controller.postMessage('hoge');
ServiceWorkerGlobalScope message event: Service worker がクライアントから受け取る。
// service worker side
self.addEventListener('message', event => {
switch (event.data) {
case hoge':
...
}
}
Service worker からクライアントにメッセージを送る。
Client postMessage: Service worker からクライアントに送る。クライアントのリストは self.clients から取得出来るが、返るのが Client のリストの Promise なのでややこしい。
// service worker side
self.clients.matchAll().then(clients =>
clients.forEach(client => client.postMessage({'hage' : hage})));
ServiceWorkerContainer message event: クライアントが Service worker から受け取る。
// client side
navigator.serviceWorker.addEventListener('message', onWorkerMessage);
これらを使うと、複数のクライアントと一つの Service Worker が連携するクライアント・サーバ的な事が出来ます。さらに、Service worker はクライアントが無くてもバックグラウンドで動き続けようとするのですが、setInterval を使って調べてみると、数分から数十秒の勝手なタイミングで止まってしまうのでデーモン的には使えないです。
Background Sync で接続イベントを受ける
Service Worker を使うと、オフラインからオンラインになった瞬間にバックグラウンドで何か面白い事が出来る。使い所としては、オフラインでメールを書いた後でオンラインになった時にバックグラウンドで送信したい時等に使えるらしい。
まず、sync の受信を宣言するにはクライアント側で SyncManager の register() を使う。ここでは、'outbox' という名前のタグを使って sync の要求元を区別する。
// client side
navigator.serviceWorker.ready.then(registration =>
registration.sync.register('outbox')
.then(() => console.log('sync registration success'))
.catch(error => console.log('sync registration error', error)));
Service worker 側の sync イベントハンドラでメッセージを受け取る。もしも register 時にオンラインならばすぐに sync イベントハンドラは呼ばれます。
// service worker side
self.addEventListener('sync', event => {
if (event.tag == 'outbox') {
console.log('Worker: sync is signaled.', now);
}
});
この機能のすごい所は、画面に Web アプリが表示されていなくても動く所です。オフライン時に sync を登録すると、そのアプリの画面を閉じた後でもオンラインになった時に sync イベントハンドラが呼ばれます。
参考にしたサイト https://github.com/WICG/BackgroundSync/blob/master/explainer.md には他に定期的に Service Worker を呼び出す。periodicSync というのも紹介されていましたが、Chrome に実装されていなかったので無視しました。
Service worker の更新
Service worker のソースコードを変更すると登録された Service worker もあるルールに則って更新される。ここが結構凝っている。大体こんなルールになっている。
- 最初のバージョンはリロードするまで実行しない。
- 強制的に実行するには activate イベントで self.clients.claim() を呼ぶ。
- ファイルを更新した後でページ遷移が発生すると、自動的に install イベントまでは進む。
- 旧バージョンを実行している Client が全部閉じるまで待つ。リロードしても閉じた事にはならない。
- 強制的に新バージョンを実行するには skipWaiting() を呼ぶ。
- この場合、古い Client と新しい Service Worker の組み合わせになるので注意。
- 強制的に新バージョンを実行するには skipWaiting() を呼ぶ。
- 全部閉じると activate イベントが呼ばれる。
- このタイミングで旧キャッシュを削除。
- 新 Service worker 実行
以下に詳しく流れを追う。Client が navigator.serviceWorker.register() で Service worker を登録した時の流れ
- Service worker の install イベントが呼ばれる
- 普通はService worker はキャッシュ登録等を行う。
- 成功すると、register() が返した Promise が成功する。
- 普通はService worker はキャッシュ登録等を行う。
- Service worker の activate イベントが呼ばれる
Client をリロードするか、Service worker が claim を呼んだ時。
- Client の navigator.serviceWorker.controller に Service worker を登録する。
- Client からのネットワークアクセスを Service worker の fetch で制御出来るようになる。
Service worker の JavaScript ファイルを更新した時。
-
Service Worker のアップデート で説明されたタイミングまで何もしない。
- スコープ内ページへの遷移、24 時間以上たってからのイベント発生時、register() 呼び出し時らしい。
- Service worker の install イベントが呼ばれる。
- 強制的に更新するにはここで skipWaiting() を呼ぶ。
- 試しに古い Service worker で skipWaiting を呼んでも何も起こらなかった。
- 強制的に更新するにはここで skipWaiting() を呼ぶ。
- 既存の Service worker の client が全部いなくなるまで待機。
- Service worker の activate イベントが呼ばれ、Client が新しいバージョンの Service worker と関連付けられる。
開発のコツ
- chrome://serviceworker-internals/ や chrome://inspect/#service-workers でインストールされた Service Worker を見ることが出来る。
参考
- 仕様: https://w3c.github.io/ServiceWorker/
- Service Worker のライフサイクル: https://developers.google.com/web/fundamentals/instant-and-offline/service-worker/lifecycle?hl=ja
- W3C の紹介: https://github.com/w3c/ServiceWorker/blob/master/explainer.md
- Google の紹介: https://developers.google.com/web/fundamentals/getting-started/primers/service-workers?hl=ja
- Service Worker のデバッグ: https://developers.google.com/web/fundamentals/getting-started/codelabs/debugging-service-workers/?hl=ja
- MDN: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
- scope について: http://qiita.com/y_fujieda/items/f9e765ac9d89ba241154
- 図が良い: https://html5experts.jp/horo/21360/
- polyfill: https://github.com/dominiccooney/cache-polyfill