Service Worker は独自のライフサイクルを持っている。ブラウザにインストールされ、有効化され、そして新しい Service Worker に置き換えられる。
Service Worker を正しく使うためには、このライフサイクルに対する理解が不可欠である。これを理解していないと、意図した通りに動かせず、古い Service worker が動作し続けてしまうなどの不具合を起こしてしまう恐れがある。
そのためこの記事では、Service Worker はどのようなライフサイクルを辿るのかを見ていく。
また、Service Worker の挙動には「スコープ」という概念も影響してくるため、スコープについても説明する。
プッシュ通知やオフライン対応などの、Service Worker を使うとどんなことが出来るようになるのか、といったことについては扱わない。それらの機能の基盤である 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
を読み込むようになる。
ユーザーが初めてページにアクセスした状況を再現するために、シークレットウィンドウでページを開くようにした。
登録
Service Worker を利用するためにはまず、Service Worker が書かれたファイルを読み込む必要がある。そこからライフサイクルが始まる。
まず、以下の Service Worker ファイルを用意する。
// public/sw1.js console.log('This is sw1.js');
何もせず、ただログを流すだけのスクリプト。
そしてこれを、以下のHome
コンポーネントで読み込むようにする。
読み込みにはnavigator.serviceWorker.register
メソッドを使う。
このメソッドは promise を返し、登録に成功するとその promise を解決する。
// pages/index.js import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js').then(() => { console.log('registered'); }); }, []); return <p>Hello Service Worker!</p>; }
next dev
を実行して開発サーバを起動する。
この状態でhttp://localhost:3000/
にアクセスすると、This is sw.js
とregistered
がログに流れる。
これで、Service Worker の登録は完了した。
ページを何度リロードしても、registered
はその度に表示されるのに対し、This is sw.js
は最初の一度しか表示されない。
ブラウザは/sw1.js
の内容をチェックしており、その内容に変更がない場合は「更新がない」と見做して、/sw1.js
の内容を再度実行することはない。
試しに/sw1.js
の内容を更新してからページをリロードすると、/sw1.js
の内容が実行されるはずである。
この更新確認に関する挙動については、後述する。
登録しようとしたファイルが存在しない場合、登録に失敗する。
その場合、register
が返した promise はリジェクトされる。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker .register('/foo.js') // 存在しないファイルを登録しようとしている .then(() => { console.log('registered'); // これは実行されない }) .catch(() => { console.log('register failed'); // これが実行される }); }, []); return <p>Hello Service Worker!</p>; }
Service Worker が例外を投げたときも同様に、登録に失敗する。
// public/sw1.js throw new Error();
登録に成功すると、register
が返した promise はServiceWorkerRegistration
で解決される。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js').then((reg) => { console.log(reg.constructor.name); // ServiceWorkerRegistration }); }, []); return <p>Hello Service Worker!</p>; }
このServiceWorkerRegistration
は、Service Worker 側のスクリプトでもself.registration
で取得できる。
// public/sw1.js console.log(self.registration.constructor.name); // ServiceWorkerRegistration
ServiceWorkerRegistration
を使うことで登録された Service Worker に関する情報を取得できる。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js').then((reg) => { console.log(reg.scope); // http://localhost:3000/ console.log(reg.updateViaCache); // imports }); }, []); return <p>Hello Service Worker!</p>; }
ServiceWorkerRegistration
については後述する。
まずは、ライフサイクルの続きを見ていく。
インストール
登録に成功するとinstall
イベントが発生する。
Service Worker 側でinstall
イベントにハンドラを設定しておくと、インストール時にそのハンドラが実行される。
self.addEventListener('install', () => { console.log('install'); });
ハンドラに渡されるイベントオブジェクトにはwaitUntil
メソッドがある。
このメソッドを使うことで、任意の処理が終わるまでinstall
イベントの完了を待つことができる。
例えばinstall
イベントハンドラでは、ファイルのキャッシュなどを行うことが多い。
ファイルのキャッシュは非同期で行われるが、それが終わる前にinstall
イベントが終わってしまうと困る。
waitUntil
を使えば、ファイルのキャッシュが終わるまでinstall
イベントは終了しない。
具体的には、waitUntil
に promise を渡して使う。
そうすると、渡された promise が解決された時点でinstall
イベントが終了するようになる。
また、渡された promise がリジェクトされるとインストール処理が失敗したと見做されるため、インストール処理の成否を伝える手段としても使える。
以下のコードだと、someTask()
が例外を投げると promise がリジェクトされるため、インストールが失敗したと見做される。
self.addEventListener('install', (e) => { e.waitUntil( new Promise((resolve, reject) => { try { someTask(); resolve(); } catch (err) { reject(err); } }) ); });
アクティベート
install
イベントが完了すると、activate
イベントが発生する。
// install // activate // の順でログに流れる self.addEventListener('install', () => { console.log('install'); }); self.addEventListener('activate', () => { console.log('activate'); });
先程インストールを失敗させる方法を書いたが、その場合、activate
イベントは発生しない。
// install // のみがログに流れる self.addEventListener('install', (e) => { console.log('install'); e.waitUntil(Promise.reject()); }); self.addEventListener('activate', () => { console.log('activate'); });
install
イベントが完了しない限りactivate
イベントは発生しない。
そのため以下のコードだと、Service Worker を登録してから約3
秒後にactivate
がログに流れる。
function sleep(ms) { const startTime = performance.now(); while (performance.now() - startTime < ms); } self.addEventListener('install', (e) => { e.waitUntil( new Promise((resolve) => { sleep(3000); resolve(); }) ); }); self.addEventListener('activate', () => { console.log('activate'); });
activate
イベントでも、install
イベント同様にwaitUntil
を使って処理を行う。
使い方は同じで、waitUntil
に渡された promise が解決されるまでactivate
イベントは終了しない。
だがactivate
イベントの場合、promise がリジェクトされても「アクティベートの失敗」とはならず、アクティベートは完了となってしまう。
そしてそのままライフサイクルは続き、コントローラー(後述)になれてしまう。
// 問題なくアクティベートが完了する self.addEventListener('activate', (e) => { e.waitUntil(Promise.reject()); });
navigator.serviceWorker.ready
を使うことで、activate
イベントが発生したかを知ることができる。
このプロパティの値は promise で、activate
イベントが発生しない限りpending
であり続ける。
そのため以下のコードだと、何度check
ボタンを押下しても promise はpending
のままである。
export default function Home() { const onClick = () => { console.log(navigator.serviceWorker.ready); // Promise {<pending>} }; return ( <> <p>Hello Service Worker!</p> <button type="button" onClick={onClick}> check </button> </> ); }
この promise は、activate
イベントが発生する直前にServiceWorkerRegistration
で解決される。
activate
イベントが完了したら、ではないことに注意する。
そのため以下のコードでは、activate
がログに流れる前にcheck
ボタンを押下してもnavigator.serviceWorker.ready
は解決されており、ログにServiceWorkerRegistration
が流れる。
function sleep(ms) { const startTime = performance.now(); while (performance.now() - startTime < ms); } self.addEventListener('activate', (e) => { e.waitUntil( new Promise((resolve) => { sleep(5000); console.log('activate'); resolve(); }) ); });
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js'); }, []); const onClick = () => { navigator.serviceWorker.ready.then((reg) => { console.log(reg.constructor.name); // ServiceWorkerRegistration }); }; return ( <> <p>Hello Service Worker!</p> <button type="button" onClick={onClick}> check </button> </> ); }
開いているページの URL が登録された Service Worker のスコープ外であった場合、その Service Worker でactivate
イベントが発生しても、navigator.serviceWorker.ready
は解決されない。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { // http://localhost:3000/ を開いているが Service Worker のスコープは http://localhost:3000/foo navigator.serviceWorker.register('/sw1.js', {scope: '/foo'}); }, []); const onClick = () => { console.log(navigator.serviceWorker.ready); // 常に Promise {<pending>} }; return ( <> <p>Hello Service Worker!</p> <button type="button" onClick={onClick}> check </button> </> ); }
スコープについては後述する。
コントローラーになる
Service Worker には「コントロール」や「コントローラー」という概念がある。
Service Worker がページを制御している状態を、そのページをコントロールしている、と表現する。
そして制御している Service Worker のことを、コントローラーと呼ぶ。
コントローラーとなった Service Worker は例えば、コントロールしているページに対するリクエストを制御し、予めキャッシュしておいたリソースをレスポンスとして返す、といったことが可能になる。
ここまで、Service Worker の登録、インストール、アクティベートを見てきたが、これだけではまだ、Service Worker はコントローラーにはなっていない。
今開いているページをコントロールしている Service Worker が何であるかは、navigator.serviceWorker.controller
で確認できる。
以下のコードでは、check
ボタンを押下すると、コントローラーが存在するかを調べる。
そうすると、何度押下してもnavigator.serviceWorker.controller
はnull
のままである。
ログが流れるということはnavigator.serviceWorker.ready
が解決しているということであり、既にactivate
イベントは発生している。
にも関わらず、コントローラーにはなっていないのである。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js'); }, []); const onClick = () => { navigator.serviceWorker.ready.then(() => { console.log(navigator.serviceWorker.controller); // null console.log(navigator.serviceWorker.controller?.scriptURL); // undefined }); }; return ( <> <p>Hello Service Worker!</p> <button type="button" onClick={onClick}> check </button> </> ); }
コントローラーになるためには、ページをリロードする必要がある。
ページをリロードしてから押下すると、ServiceWorker
オブジェクトがログに表示され、scriptURL
はhttp://localhost:3000/sw1.js
であることが分かる。
これは、http://localhost:3000/sw1.js
がこのページのコントローラーであることを意味している。
ページをコントロールするかどうかは、そのページを開いたときにアクティベートされているかで、判断される。
そのため、このような挙動になるのである。
整理すると以下のようになる。
http://localhost:3000/
にアクセスする- この時点では、Service Worker は何も登録されていないため、このページをコントロールする Service Worker は存在しない
http://localhost:3000/sw1.js
が登録され、アクティベートが行われる- ページをコントロールするかどうかの判定はページを開いたときに行われているので、
http://localhost:3000/sw1.js
が登録されアクティベートされたところで、このページをコントロールする Service Worker が存在しないことには変わりはない(navigator.serviceWorker.controller
はnull
のまま) - (リロードするなどして)
http://localhost:3000/
に再訪する http://localhost:3000/sw1.js
が登録されており、かつアクティベートされているので、この Service Worker がhttp://localhost:3000/
をコントロールするようになる(navigator.serviceWorker.controller
がServiceWorker
オブジェクトになる)
これがデフォルトの挙動だが、ページを開き直すのを待たず、即座にコントロールを開始したいケースもある。
そのようなときはself.clients.claim
を使う。
このメソッドを使うと、スコープ内のページのコントローラーとして自分自身(この記事の例だと/sw1.js
)を設定できる。
Service Worker ファイルを以下の内容にすると、アクティベートされた時点で、コントローラーとなる。先程のようにリロードする必要はない。
claim
は promise を返すので、そのままwaitUntil
に渡すことができる。
self.addEventListener('activate', (e) => { e.waitUntil(self.clients.claim()); });
install
イベントのなかでclaim
を呼び出そうとするとエラーになる。
self.addEventListener('install', (e) => { e.waitUntil(self.clients.claim()); // Only the active worker can claim clients. });
navigator.serviceWorker.controller
が変化すると、controllerchange
イベントが発生し、navigator.serviceWorker.oncontrollerchange
が呼び出される。
このメソッドの初期値はnull
なので、デフォルトだと何も発生しない。
以下のコードでは、controllerchange
イベントが発生するとログにコントローラーが流れるようにしている。
そしてregister
ボタンを押下すると Service Worker の登録が行われるため、押下すると登録とclaim
の実行が発生しコントローラーが設定され、ServiceWorker
オブジェクトが流れる。
self.addEventListener('activate', (e) => { e.waitUntil(self.clients.claim()); });
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.addEventListener('controllerchange', () => { console.log(navigator.serviceWorker.controller); }); }, []); const onClick = () => { navigator.serviceWorker.register('/sw1.js'); }; return ( <> <p>Hello Service Worker!</p> <button type="button" onClick={onClick}> register </button> </> ); }
スコープ
Service Worker には必ずスコープが設定されており、そのスコープ内のページのみをコントロールする。
スコープから外れているページはコントロールしない。
スコープは、register
で Service Worker を登録するときに設定される。
register
の第 2 引数で設定可能で、省略した場合は Service Worker ファイルが置かれているディレクトリがスコープになる。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker .register('/sw1.js', {scope: '/foo'}) .then((reg) => { console.log(reg.scope); // http://localhost:3000/foo }); }, []); return ( <> <p>Hello Service Worker!</p> </> ); }
スコープを/foo
に設定するとhttp://localhost:3000/
はスコープ外になり、登録した Service Worker によってコントロールされることはない。
以下のコードではcheck
ボタンを押下するとnull
がログに流れる。
もちろんページを何度リロードしても、挙動は変わらない。
// public/sw1.js self.addEventListener('activate', (e) => { e.waitUntil(self.clients.claim()); });
// pages/index.js import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js', {scope: '/foo'}); }, []); const onClick = () => { console.log(navigator.serviceWorker.controller); // null }; return ( <> <p>This page is top.</p> <p> <button type="button" onClick={onClick}> check </button> </p> </> ); }
この状態で以下の内容のhttp://localhost:3000/foo
にアクセスしボタンを押下すると、コントローラーが存在していることを確認できる。
Service Worker のスコープは/foo
であり、このページはそのスコープ内だからである。
// pages/foo/index.js export default function Foo() { const onClick = () => { console.log(navigator.serviceWorker.controller); // ServiceWorker }; return ( <> <p>This page is foo.</p> <p> <button type="button" onClick={onClick}> check </button> </p> </> ); }
スコープは、「コントロールするページ」の範囲を決めるものに過ぎない。
そのため、コントロールしているページからのリクエストについては、スコープの制御を受けない。
例えば、/foo
からfetch
で/api/x
にリクエストを飛ばした場合、そのリクエストも Service Worker でコントロールできる。
/api/x
はスコープから外れているが、関係ない。
// public/sw1.js self.addEventListener('activate', (e) => { e.waitUntil(self.clients.claim()); }); // 本筋から外れるので詳細な説明は省くが、/api/ で始まるパスへのリクエストを制御し、abc を返すようにしている self.addEventListener('fetch', (e) => { const url = new URL(e.request.url); if (/^\/api\//.test(url.pathname)) { e.respondWith(new Response('abc')); } });
// pages/foo/index.js import {useEffect} from 'react'; export default function Foo() { useEffect(() => { navigator.serviceWorker.register('/sw1.js', {scope: '/foo'}); }, []); const onClick = async () => { const res = await fetch('/api/x'); const text = await res.text(); console.log(text); // abc }; return ( <> <p>This page is foo.</p> <p> <button type="button" onClick={onClick}> fetch </button> </p> </> ); }
スコープは、登録する Service Worker ファイルが置かれているディレクトリか、それより下の階層しか、設計できない。
例えば以下の例だと、Service Worker ファイルが置かれているのは/foo/
ディレクトリなのに、それより上位の/
をスコープとして設定しようとしている。
そのためエラーになり、登録に失敗している。
useEffect(() => { navigator.serviceWorker.register('/foo/sw2.js', {scope: '/'}).catch((e) => { // Failed to register a ServiceWorker for scope ('http://localhost:3000/') with script ('http://localhost:3000/foo/sw2.js') console.log(e.message); }); }, []);
下位の階層なら、問題ない。
useEffect(() => { navigator.serviceWorker .register('/foo/sw2.js', {scope: '/foo/bar/buz'}) .then((reg) => { console.log(reg.scope); // http://localhost:3000/foo/bar/buz }); }, []);
この制限を解除したい場合、HTTP ヘッダのService-Worker-Allowed
フィールドを使う。
Service Worker ファイルを返す HTTP レスポンスのヘッダにService-Worker-Allowed: /
をつけた場合、/
以下のスコープを自由に設定できるようになる。
Next.js ではnext.config.js
を編集すると独自フィールドを付与できるので、以下の内容にして確認してみる。
// next.config.js module.exports = { async headers() { return [ { source: '/foo/sw2.js', headers: [ { key: 'Service-Worker-Allowed', value: '/', }, ], }, ]; }, };
Service Worker ファイルは/foo/
に置かれているが、それよりも上位のパスである/
をスコープに設定できるようになった。
useEffect(() => { navigator.serviceWorker .register('/foo/sw2.js', {scope: '/'}) .then((reg) => { console.log(reg.scope); // http://localhost:3000/ }); }, []);
更新確認
ここまで、Service Worker が登録されてからコントローラーになるところまでを見てきた。
コントローラーになったということはページを制御しているということであり、Service Worker を基盤とした様々な機能を利用できるようになったということである。
だが Service Worker のライフサイクルはこれで終わりではない。
インストールされた Service Worker はページが閉じられた後もブラウザに残り続けるが、これをそのままにしておくと、古い Service Worker がいつまでも残り続けてしまう。
そのためブラウザは定期的に、Service Worker ファイルに更新がないかをサーバに対してチェックしている。そしてもし更新があった場合、更新後の Service Worker ファイルをインストールし、以前インストールした Service Worker は所定のタイミングで破棄される。
これから更新確認について説明していくが、いつ更新確認を行うのかはブラウザによって異なる可能性がある。
冒頭にも書いたように、この記事では Google Chrome 90.0.4430.212 で動作確認を行っている。
復習になるが、以下のページを表示するとinstall
がログに流れるが、それ以降は何回リロードしてもinstall
は流れない。
self.addEventListener('install', () => { console.log('install'); });
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js'); }, []); return ( <> <p>Hello Service Worker!</p> </> ); }
リロードされる度にブラウザは更新確認を行っており、/sw1.js
の内容に変化がないため、更新がないと判断された。
更新があった場合は、改めて/sw1.js
が読み込まれ、install
イベントが発生する。
http://localhost:3000/
を開きっぱなしにした状態で/sw1.js
を以下の内容に更新し、ページをリロードしてみる。
console.log('v2'); self.addEventListener('install', () => { console.log('installed v2'); });
そうすると、v2
とinstalled v2
がログに流れる。
更新確認の結果、/sw1.js
が更新されていると判定されたためである。
Service Worker ファイルそのものだけではなく、importScripts
で読み込んだファイルも、更新確認の対象になる。
importScripts
についてはこの記事では詳述しないが、このメソッドを使って他のファイルを読み込める。
// sw1.js importScripts('/someFile.js'); self.addEventListener('install', () => { console.log('install'); });
このとき、/someFile.js
が更新されれば、/sw1.js
は更新がなかったとしても、/sw1.js
自体が改めて読み込まれて実行される。
更新確認の際、ブラウザは常にサーバに確認しに行くわけではない。
最初からサーバに確認しに行くこともあれば、ブラウザにキャッシュされているファイルを使って確認することもある。
後者の場合、キャッシュが存在しなかったときにのみ、サーバに対して確認する。
この挙動は、登録時に設定したupdateViaCache
で決まる。
navigator.serviceWorker.register('/sw1.js', {updateViaCache: 'all'});
値に応じて、以下の挙動になる。
register されたファイル | インポートされたファイル | |
---|---|---|
all | キャッシュをまず確認する | キャッシュをまず確認する |
imports | 最初からサーバに確認しに行く | キャッシュをまず確認する |
none | 最初からサーバに確認しに行く | 最初からサーバに確認しに行く |
設定を省略した場合はimports
になる。
そのため、インポートされたファイルについてのみ、まずはキャッシュされたファイルを確認する。
再びnext.config.js
を編集して、確認してみる。
module.exports = { async headers() { return [ { source: '/(.*).js', headers: [ { key: 'Cache-Control', value: 'max-age=10', }, ], }, ]; }, };
これで、/sw1.js
も/someFile.js
も10
秒間キャッシュされるようになった。
そのため、ページ表示から10
秒以内にimportScripts
で読み込んだファイル(今回の例でいえば/someFile.js
)を更新してページをリロードしても、Service Worker の更新処理は行われない。
但し 24 時間以上経過した場合は、updateViaCache
とは関係なく必ずサーバに確認しに行く。
そうすることで、設定ミスなどによって古い Service Worker が残り続けてしまうのを防いでいる。
アクティベートのタイミングについて
ファイルが更新されると更新後のファイルが読み込まれinstall
イベントが発生することを確認したが、activate
はすぐには発生しない。
確認用に作った以下の Service Worker ファイルではclaim
を使っており、ページ表示後すぐにアクティブになる。
そのため、fetch
ボタンを押下すると、v1
がログに流れる。
const VERSION = 'v1'; self.addEventListener('install', () => { console.log(`install ${VERSION}`); }); self.addEventListener('activate', (e) => { console.log(`activate ${VERSION}`); e.waitUntil(self.clients.claim()); }); self.addEventListener('fetch', (e) => { const url = new URL(e.request.url); if (/^\/api\//.test(url.pathname)) { e.respondWith(new Response(VERSION)); } });
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js'); }, []); const onClick = async () => { const res = await fetch('/api/x'); const text = await res.text(); console.log(text); }; return ( <> <p>Hello Service Worker!</p> <p> <button type="button" onClick={onClick}> fetch </button> </p> </> ); }
ページを開いたままにして、VERSION
の値をv1
からv2
を変える。
そしてページをリロードすると、install v2
が表示される。
だが、activate v2
は表示されない。
つまり、アクティベートされていないのである。事実、fetch
ボタンを押下するとv1
が流れる。相変わらずv1
の Service Worker がこのページをコントロールしている。
v2
をアクティベートするためには、v1
がコントロールしているページを全て閉じる必要がある。
そうすると、新しくhttp://localhost:3000/
を開いたときに改めてv2
のインストール、そしてアクティベートが行われる。
今回のコードではclaim
を使っているので、そのままv2
がコントローラーになる。
そしてv1
は無効となり、この Service Worker のライフサイクルは終わりを迎える。
更新後の Service Worker をすぐにアクティベートしたい場合は、self.skipWaiting
を使う。
self.addEventListener('install', (e) => { console.log(`install ${VERSION}`); e.waitUntil(self.skipWaiting()); });
このメソッドを使うと、即座にその Service Worker ファイルがアクティベートされるようになる。
skipWaiting
によって古い Service Worker から置き換えられた Service Workerは、そのままページのコントローラーになる。
ServiceWorkerRegistration について
ライフサイクルの説明は一通り終わったので、最後にServiceWorkerRegistration
について説明する。
Service Worker の登録を行うregister
メソッドは、登録に成功するとServiceWorkerRegistration
で解決する promise オブジェクトを返す。
ここまで説明してきたスコープやupdateViaCache
も、ServiceWorkerRegistration
で確認できる。
useEffect(() => { navigator.serviceWorker.register('/sw1.js').then((registration) => { console.log(registration); // ServiceWorkerRegistration console.log(registration.scope); // http://localhost:3000/ console.log(registration.updateViaCache); // imports }); }, []);
ServiceWorkerRegistration
はnavigator.serviceWorker.getRegistration
でも取得できる。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js'); }, []); const onClick = async () => { const registration = await navigator.serviceWorker.getRegistration(); console.log(registration); // ServiceWorkerRegistration }; return ( <> <p>Hello Service Worker!</p> <p> <button type="button" onClick={onClick}> check </button> </p> </> ); }
ServiceWorkerRegistration
が持っているupdate
メソッドを使うと、明示的に更新確認を行える。
以下のコードでは、update
ボタンを押下したときにupdate
メソッドを実行している。
そのため、VERSION
の値をv2
に変えてからupdate
ボタンを押下すると、v2
の Service Worker がアクティベートされ、そのままページのコントローラーになる。
ページをリロードする必要はない。
const VERSION = 'v1'; self.addEventListener('install', (e) => { console.log(`install ${VERSION}`); e.waitUntil(self.skipWaiting()); }); self.addEventListener('activate', (e) => { console.log(`activate ${VERSION}`); e.waitUntil(self.clients.claim()); }); self.addEventListener('fetch', (e) => { const url = new URL(e.request.url); if (/^\/api\//.test(url.pathname)) { e.respondWith(new Response(VERSION)); } });
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js'); }, []); const fetchApi = async () => { const res = await fetch('/api/x'); const text = await res.text(); console.log(text); }; const update = async () => { const registration = await navigator.serviceWorker.getRegistration(); registration.update(); }; return ( <> <p>Hello Service Worker!</p> <p> <button type="button" onClick={fetchApi}> fetch </button> </p> <p> <button type="button" onClick={update}> update </button> </p> </> ); }
ServiceWorkerRegistration
にはunregister
というメソッドもある。
このメソッドを使うと、Service Worker の登録を抹消できる。
登録解除後にnavigator.serviceWorker.getRegistration()
を実行するとundefined
で解決された promise が返ってくるので、登録が抹消されていることが分かる。
だが登録が抹消されても、Service Worker は引き続きページをコントロールしている。
以下のコードでそれを確認できる。
抹消後もnavigator.serviceWorker.controller
にはServiceWorker
オブジェクトが入っている。
つまり Service Worker によってコントロールされているということであり、事実、fetchApi
ボタンを押下すると、Service Worker によって制御された結果が返ってくる。
import {useEffect} from 'react'; export default function Home() { useEffect(() => { navigator.serviceWorker.register('/sw1.js'); }, []); const fetchApi = async () => { const res = await fetch('/api/x'); const text = await res.text(); console.log(text); }; const unregister = async () => { const registration = await navigator.serviceWorker.getRegistration(); registration?.unregister(); console.log(await navigator.serviceWorker.getRegistration()); // undefined console.log(navigator.serviceWorker.controller); // ServiceWorker }; return ( <> <p>Hello Service Worker!</p> <p> <button type="button" onClick={fetchApi}> fetch </button> </p> <p> <button type="button" onClick={unregister}> unregister </button> </p> </> ); }
この Service Worker がコントロールしているページを全て閉じたときに、controller
はnull
になる。