Blazor WebAssembly オフライン対応 PWA をリロードしても更新できないのはなぜか?

## 😥 はじめに

オフラインモードに対応した Blazor WebAssembly の PWA サイトを持っていて、その PWA を Web ブラウザで開いていた場合を想定してください。
その PWA の新バージョンを発行・配置した際に、その新しいバージョンの内容をすぐに確認したいことがあるかもしれません。その場合は、その Web ブラウザで当該 PWA のページを再読み込み (リロード) するかと想像されます。

しかし、期待に反して、ハードリロードしても新バージョンの内容は表示されず**前バージョンのコンテンツが引き続き表示されるだけ**になるかもしれません。ブラウザの開発者ツールウィンドウからキャッシュを無効にしたり、ブラウザの再読み込みアイコンボタンを右クリックして「キャッシュを空にしてハードリフレッシュ」メニューをクリックしても、**全く効果がありません**😱。

その PWA の新バージョンを有効にするには、その PWA サイトを開いているブラウザの「すべての」タブを**一度閉じる必要があります**。一度閉じた後で改めてその PWA サイトを開き直すと、ようやく新バージョンのコンテンツが表示されます。
(※ユーザーの立場ではなく開発者の立場であれば、ブラウザの開発者ツールを開き、「アプリケーション」タブ→サービスワーカーから、手動による更新もできます)

## 🤔 この動作は "意図的 "です。

これは、Web ブラウザの仕様と、Visual Studio や .NET SDK のプロジェクトテンプレートで生成されるサービスワーカー JavaScript のデフォルト実装から来る仕様です。

まず最初に、**サービスワーカーは "アトミックに" 新しいバージョンに更新される**ことを説明しなければなりません。Web ブラウザが新しいバージョンのサービスワーカー JavaScript コードを検出すると、Web ブラウザはそれを読み込みはするのですが、「起動待ち」状態のままにしておくのです。これは、Web プラットフォームの標準的な動作です。サービスワーカーは、すべてのブラウザタブで共有されるプロセスであり、オフラインサポートのキャッシュ機構を制御します。したがって、その更新メカニズムが「アトミック」でないと、同じ PWA の異なるタブが異なる動作をしたり、現在のバージョンのページコンテンツが新しいバージョンのコンテンツで汚染されるなど、重大な混乱を引き起こします。これが、サービスワーカー、ひいてはそのサービスワーカーによって制御されるオフライン動作用キャッシュが、アトミックに切り替わる仕様の存在理由です。

## ⚙️ すべてのタブを閉じずに次のバージョンに更新する

しかしそうは言っても、場合によっては、すべてのタブを閉じることなく、すぐに次のバージョンにアップデートできる PWA サイトを作る必要に迫られることがあるかもしれませんね。

そのような場合は、次のバージョンの準備ができたことをユーザーインターフェースを通じてユーザーに通知するのが妥当な方法かと思います。そして、ユーザーがそのユーザーインターフェースに応答して次のバージョンに明示的に更新を実施する場合は、サービスワーカーの [`skipWaiting()`メソッド](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting)を使用してその動作を実装することができます。

ただ、少々残念なことに、具体的な実装は以下のとおり若干複雑になります。

1. サービスワーカープロセスの `skipWaiting()` メソッドを、ユーザーとの対話コードなどの外部から呼び出せるようにします。具体的には、特定の「メッセージ」をハンドルするようサービスワーカー JavaScript を実装し、そのメッセージハンドラ内で `skipWaiting()` メソッドを呼び出せばよいでしょう。

2. "The next version is ready (新しいバージョンの準備ができました)" のようなメッセージと "Update now (今すぐ更新)" のようなボタンを表示するユーザーインターフェイスを実装します。そしてユーザがそのユーザーインターフェース上の "Update now" ボタンをクリックしたときにサービスワーカープロセスに特定のメッセージを送り、サービスワーカープロセスの `skipWaiting()` メソッドが実行されるようにします。

3. `navigator.serviceWorker.register` メソッドの Promise コールバックをハンドルします。

4. 上記 Promise コールバック内にて、"待機中 "のサービスワーカープロセス（つまり新しいバージョンのサービスワーカー）があるかチェックし、もしある場合は、次のバージョンの準備ができたことを前述のユーザーインターフェースを介してユーザーに通知します。

5. 続けて、サービスワーカー登録オブジェクトの `updatefound` イベントを購読します。`updatefound` イベントが発生したら、さらに続けて、今度はサービスワーカープロセスの `statechange` イベントを購読します。

6. サービスワーカープロセスの `statechange` イベントが発生したら、そのサービスワーカープロセスの現在のステータスをチェックします。もし、ステータスが "installed" であれば、次のバージョンの準備ができたことを意味するので、その旨を前述のユーザーインターフェースでユーザーに通知します。

7. ステータスが "activated" であれば、ユーザが前述のユーザーインターフェースに応答し "Update now (今すぐ更新)" ボタンをクリックした結果として、`skipWaiting()` メソッドが実行されたことを意味するので、`window.location.reload()` メソッドを呼び出して現在のページを再読み込みします。

...どうでしょう、この説明についてこれていますか？😅

## 🎉 "Blazor PWA Updater" NuGet パッケージとコンポーネント

幸い、上記のコードをすべて自分で実装する必要はありません! 代わりに、**"Blazor PWA Updater "** NuGet パッケージとそのコンポーネントを使用することができます。

1. `Toolbelt.Blazor.PWA.Updater` NuGet パッケージをインストールします。

2. Blazor PWA の起動時処理にて、サービスプロバイダに「PWA updater」サービスを登録します。

3. 3. Blazor PWA 内の適当なコンポーネント内 (`App.razor` とか `MainLayout.razor` とか) に `<PWAUpdater>` コンポーネントを配置します。

4. `service-worker.published.js` ファイルに `"SKIP_WAITING"` メッセージハンドラを追加し、そのメッセージハンドラ内で `skipWaiting()` メソッドを呼び出すように実装します。

5. `index.html` 内のスクリプト要素 `<script>navigator.serviceWorker.register('service-worker.js');</script>` を、`<script src="_content/Toolbelt.Blazor.PWA.Updater.Service/script.min.js"></script>` に書き換えます。

以上です。

上記の実装を行うと、Blazor PWA 上で、すぐに新しいバージョンに更新するユーザーインターフェースとその機能が得られます。

![](https://raw.githubusercontent.com/jsakamoto/Toolbelt.Blazor.PWA.Updater/main/.assets/movie.001.gif)

また、通知テキスト、ボタンキャプション、テキスト色、背景色、サイズ、位置などといった、`<PWAUpdater>` の外観をカスタマイズすることもできます。

さらには、`<PWAUpdater>` の外観や動作が全く好みでない場合は、最小限の労力でゼロからユーザーインターフェースを実装することも可能です。NuGet パッケージの `Toolbelt.Blazor.PWA.Updater.Service` に含まれる `IPWAUpdaterService` サービスは `NextVersionIsWaiting` イベントと `SkipWaitingAsync()` メソッドを公開しており、DI からこのサービスを入手することで、残るユーザーインターフェイスを自分で好きなように構築することができます。

詳しくは「Blazor PWA Updater」の GitHub リポジトリにアクセスし、README を参照ください。

https://github.com/jsakamoto/Toolbelt.Blazor.PWA.Updater

## ⚠️注意

なお、このような更新通知のユーザーインターフェースが頻繁に表示されると、**ユーザーが煩わしく感じることもある**点を心に留めておいてください。

もし、新しいバージョンをすぐに更新するような通知ユーザーインターフェイスを実装する十分な理由がないのであれば、デフォルトの実装のままにしておくのがよいでしょう。

_Happy Coding :)_