ServiceWorker
オブジェクトはstate
というプロパティを持っており、その名の通り Service Worker の状態を示している。
この値は状況に応じて移り変わっていくのだが、どのようなタイミングでどう変化するのかについてメンタルモデルを構築できると、Service Worker の挙動を理解しやすくなる。
この記事では、state
が変化するタイミングや、state
に関連する他のプロパティやメソッドについて見ていく。
Service Worker の基本的な概念や使い方については、以下の記事にまとめてある。
動作確認には Next.js と Google Chrome を使っている。それぞれのバージョンは以下の通り。
- Next.js 10.2.0
- Google Chrome 90.0.4430.212
特に言及がない限り、public/
に置いたsw1.js
というファイルを、pages/index.js
に書いたコンポーネントで読み込んでいる。
これにより、http://localhost:3000/
にアクセスしたときに、http://localhost:3000/sw1.js
を読み込むようになる。
ユーザーが初めてページにアクセスした状況を再現するために、シークレットウィンドウでページを開くようにしている。
state は単方向に変化する
state
には必ず文字列が入るが、その種類は 5 つある。
そして、以下に挙げる順番に変化していく。
installing
installed
activating
activated
redundant
必ずinstalling
から始まるし、以前の値に戻ることもない。
ServiceWorker オブジェクトの取得
これからstate
がどのように移り変わっていくのかを検証していくが、そのためにはまずServiceWorker
オブジェクトを取得する必要がある。
方法はいくつかあるが、この記事では基本的に以下の方法でServiceWorker
オブジェクトを取得することにする。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js').then((reg) => { console.log(reg.installing); // ServiceWorker }); }, []); return ( <> <p>Hello Service Worker!</p> </> ); }
register
が返す promise は、Service Worker の登録に成功したときにServiceWorkerRegistration
で解決される。
そしてそのServiceWorkerRegistration
にはinstalling
プロパティがあるのだが、このプロパティに、登録された Service Worker のインターフェイスであるServiceWorker
オブジェクトが入っている。
installing
の名が示す通り、インストール中の Service Worker を取得するためのプロパティだが、register
からつないだthen
メソッドのなかなら確実に、installing
プロパティにServiceWorker
が入っている。インストールが早く終わってしまってinstalling
がnull
になってしまった、という状況は発生しない。
以下のコードでそれを確認できる。sleep
関数を使って3
秒間処理をブロックしているが、その間にinstalling
がnull
になってしまうようなことはなく、ServiceWorker
オブジェクトを取得できる。
import {useEffect} from 'react'; function sleep(ms) { const startTime = performance.now(); while (performance.now() - startTime < ms); } export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js').then((reg) => { sleep(3000); console.log(reg.installing); // ServiceWorker }); }, []); return ( <> <p>Hello Service Worker!</p> </> ); }
早速state
を確認してみると、installing
になっていることが分かる。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js').then((reg) => { const sw = reg.installing; console.log(sw.state); // installing }); }, []); return ( <> <p>Hello Service Worker!</p> </> ); }
ServiceWorker
オブジェクトには、state
が変化したときに発生するstatechange
というイベントがあるので、それを使ってstate
の遷移を確認する。
以下のようにイベントハンドラを設定すれば、state
が変化する度に新しいstate
がログに流れるようになる。
sw.addEventListener('statechange', () => { console.log(sw.state); });
statechange
を使った以下のコードで、ページ表示時に登録された Service Worker がどのような状態遷移を辿るのかを知れる。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js').then((reg) => { const sw = reg.installing; console.log(sw.state); sw.addEventListener('statechange', () => { console.log(sw.state); }); }); }, []); return ( <> <p>Hello Service Worker!</p> </> ); }
以下のログが流れる。
installing installed activating activated
installing
から始まり、そのままactivated
まで変化することが分かった。
次は、それぞれの変化がどのようなタイミングで発生しているのかを見ていく。
installing -> installed or redundant
install
イベントが終了したタイミングで、installing
からinstalled
に変化する。
そのため、Service Worker のコードを以下のようにすると、install
イベントが始まった約5
秒後に、installed
に変化する。
function sleep(ms) { const startTime = performance.now(); while (performance.now() - startTime < ms); } self.addEventListener('install', (e) => { e.waitUntil( new Promise((resolve) => { sleep(5000); resolve(); }) ); });
e.waitUntil
に渡した promise がリジェクトされるとインストールが失敗したと見做され、state
はredundant
に変化する。
そのため以下のコードだと、install
イベントが始まった約5
秒後に、installing
からredundant
に変化する。
function sleep(ms) { const startTime = performance.now(); while (performance.now() - startTime < ms); } self.addEventListener('install', (e) => { e.waitUntil( new Promise((resolve, reject) => { sleep(5000); reject(); }) ); });
installed -> activating -> activated
インストールに成功しinstalling
になったServiceWorker
オブジェクトはactivating
に変化する。
そしてactivate
イベント終了のタイミングで、activated
に変化する。
従って以下のコードの場合は、すぐにactivating
にまで遷移し、その約5
秒後にactivated
になる。
function sleep(ms) { const startTime = performance.now(); while (performance.now() - startTime < ms); } self.addEventListener('activate', (e) => { e.waitUntil( new Promise((resolve) => { sleep(5000); resolve(); }) ); });
install
イベントとは異なり、e.waitUntil
に渡した promise がリジェクトされてもredundant
にはならない。
そのため以下のコードでも、activate
イベント発生の約5
秒後にactivated
になる。
function sleep(ms) { const startTime = performance.now(); while (performance.now() - startTime < ms); } self.addEventListener('activate', (e) => { e.waitUntil( new Promise((resolve, reject) => { sleep(5000); reject(); }) ); });
navigator.serviceWorker.ready
というプロパティがあり、activate
イベントが発生すると解決される promise を持っている。
そのため、このプロパティを使えばactivate
イベントの発生を感知できる。
以下の Service Worker は、install
に約2
秒かかり、activate
に約3
秒かかる。
function sleep(ms) { const startTime = performance.now(); while (performance.now() - startTime < ms); } self.addEventListener('install', (e) => { e.waitUntil( new Promise((resolve) => { sleep(2000); resolve(); }) ); }); self.addEventListener('activate', (e) => { e.waitUntil( new Promise((resolve) => { sleep(3000); resolve(); }) ); });
このとき、Service Worker を登録する側のコードが以下のようになっていた場合、登録から約2
秒後にready!
が表示され、それから約3
秒後にactivated
が表示される。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.ready.then(() => { console.log('ready!'); }); navigator.serviceWorker.register('/sw1.js').then((reg) => { const sw = reg.installing; console.log(sw.state); sw.addEventListener('statechange', () => { console.log(sw.state); }); }); }, []); return ( <> <p>Hello Service Worker!</p> </> ); }
既にコントローラーが存在する場合
ServiceWorkerRegistration
のinstalling
にServiceWorker
オブジェクトがセットされるのは、Service Worker の登録時だけではない。更新確認を行い更新があった際にも、セットされる。
installing
にServiceWorker
オブジェクトがセットされるとupdatefound
イベントが発生するので、これを使うと新しくセットされたServiceWorker
も取得できるようになる。
以下のコードのページを開き、Service Worker ファイルを更新してからupdate
ボタンを押下すると、ServiceWorker
オブジェクトがログに流れる。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js').then((reg) => { reg.addEventListener('updatefound', () => { console.log(reg.installing); // ServiceWorker }); }); }, []); const update = async () => { const registration = await navigator.serviceWorker.getRegistration(); registration.update(); }; return ( <> <p>Hello Service Worker!</p> <p> <button type="button" onClick={update}> update </button> </p> </> ); }
既にコントローラーが存在する状態で Service Worker を更新した場合、その Service Worker の状態遷移は、これまでの説明とは異なったものになる。
それを確認するために、ページのコンポーネントを以下のように書き換える。
import {useEffect} from 'react'; let counter = 0; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js').then((reg) => { reg.addEventListener('updatefound', () => { const sw = reg.installing; if (!sw) return; counter += 1; const version = `v${counter}`; console.log(version, sw.state); sw.addEventListener('statechange', () => { console.log(version, sw.state); }); }); }); }, []); const update = async () => { const registration = await navigator.serviceWorker.getRegistration(); registration.update(); }; return ( <> <p>Hello Service Worker!</p> <p> <button type="button" onClick={update}> update </button> </p> </> ); }
そして Service Worker のコードを以下のようにする。
self.addEventListener('activate', (e) => { e.waitUntil(self.clients.claim()); });
この状態でページを開くと、以下のログが流れる。
v1 installing v1 installed v1 activating v1 activated
これまでと同様、installing
からactivated
にまで遷移している。
そしてself.clients.claim
を実行しているので、この Service Worker がそのままこのページのコントローラーになる。
次に Service Worker ファイルのコードを更新する。適当にコメントを付けるなどで構わない。
そしてupdate
ボタンを押下すると、以下のログが流れる。
v2 installing v2 installed
これまでと異なり、更新後の Service Worker の状態遷移がinstalled
で止まってしまっている。
このように、コントローラーが既に存在する状態でインストールされた Service Worker は、activate
イベントが発生しないのである。
コントローラーが存在しなければactivate
イベントは発生する。そのため、self.clients.claim
を実行していないコードで同じ検証を行うと、v2
の Service Worker もすぐにactivated
にまで遷移する。
v1
がコントロールしているページを全て閉じてから改めてページを開くと、v2
がactivated
されコントローラーになっている。
もしすぐにv2
をactivated
させたい場合は、skipWaiting
を実行する。
self.addEventListener('install', (e) => { e.waitUntil(self.skipWaiting()); }); self.addEventListener('activate', (e) => { e.waitUntil(self.clients.claim()); });
この場合、Service Worker 更新後にupdate
ボタンを押下すると、v2
がすぐにactivated
にまで遷移する。
そして、v1
がredundant
に変化している。
ちなみに、既にコントローラーが存在する状態でskipWaiting
が行われたときは、更新後の Service Worker がアクティベートされるだけでなく、その Service Worker がそのままコントローラーになる。
ServiceWorkerRegistration の各種プロパティ
これまで何度か見てきたように、Service Worker のインストールが開始されるとServiceWorkerRegistration
のinstalling
プロパティにServiceWorker
オブジェクトがセットされる。
ServiceWorkerRegistration
には他にもwaiting
とactive
というプロパティを持ち、それぞれ以下のstate
のServiceWorker
オブジェクトがセットされる。
installing
プロパティstate
がinstalling
waiting
プロパティstate
がinstalled
active
プロパティstate
がactivating
もしくはactivated
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js').then((reg) => { const sw = reg.installing; console.assert(sw.state === 'installing'); sw.addEventListener('statechange', () => { if (reg.waiting) { console.assert(reg.waiting.state === 'installed'); } else if (reg.active) { console.assert( reg.active.state === 'activating' || reg.active.state === 'activated' ); } }); }); }, []); return ( <> <p>Hello Service Worker!</p> </> ); }