😥 はじめに
オフラインモードに対応した Blazor WebAssembly の PWA サイトを持っていて、その PWA を Web ブラウザで開いていた場合を想定してください。
その PWA の新バージョンを発行・配置した際に、その新しいバージョンの内容をすぐに確認したいことがあるかもしれません。その場合は、その Web ブラウザで当該 PWA のページを再読み込み (リロード) するかと想像されます。
しかし、期待に反して、ハードリロードしても新バージョンの内容は表示されず前バージョンのコンテンツが引き続き表示されるだけになるかもしれません。ブラウザの開発者ツールウィンドウからキャッシュを無効にしたり、ブラウザの再読み込みアイコンボタンを右クリックして「キャッシュを空にしてハードリフレッシュ」メニューをクリックしても、全く効果がありません😱。
その PWA の新バージョンを有効にするには、その PWA サイトを開いているブラウザの「すべての」タブを一度閉じる必要があります。一度閉じた後で改めてその PWA サイトを開き直すと、ようやく新バージョンのコンテンツが表示されます。
(※ユーザーの立場ではなく開発者の立場であれば、ブラウザの開発者ツールを開き、「アプリケーション」タブ→サービスワーカーから、手動による更新もできます)
🤔 この動作は "意図的 "です。
これは、Web ブラウザの仕様と、Visual Studio や .NET SDK のプロジェクトテンプレートで生成されるサービスワーカー JavaScript のデフォルト実装から来る仕様です。
まず最初に、サービスワーカーは "アトミックに" 新しいバージョンに更新されることを説明しなければなりません。Web ブラウザが新しいバージョンのサービスワーカー JavaScript コードを検出すると、Web ブラウザはそれを読み込みはするのですが、「起動待ち」状態のままにしておくのです。これは、Web プラットフォームの標準的な動作です。サービスワーカーは、すべてのブラウザタブで共有されるプロセスであり、オフラインサポートのキャッシュ機構を制御します。したがって、その更新メカニズムが「アトミック」でないと、同じ PWA の異なるタブが異なる動作をしたり、現在のバージョンのページコンテンツが新しいバージョンのコンテンツで汚染されるなど、重大な混乱を引き起こします。これが、サービスワーカー、ひいてはそのサービスワーカーによって制御されるオフライン動作用キャッシュが、アトミックに切り替わる仕様の存在理由です。
⚙️ すべてのタブを閉じずに次のバージョンに更新する
しかしそうは言っても、場合によっては、すべてのタブを閉じることなく、すぐに次のバージョンにアップデートできる PWA サイトを作る必要に迫られることがあるかもしれませんね。
そのような場合は、次のバージョンの準備ができたことをユーザーインターフェースを通じてユーザーに通知するのが妥当な方法かと思います。そして、ユーザーがそのユーザーインターフェースに応答して次のバージョンに明示的に更新を実施する場合は、サービスワーカーの skipWaiting()
メソッドを使用してその動作を実装することができます。
ただ、少々残念なことに、具体的な実装は以下のとおり若干複雑になります。
-
サービスワーカープロセスの
skipWaiting()
メソッドを、ユーザーとの対話コードなどの外部から呼び出せるようにします。具体的には、特定の「メッセージ」をハンドルするようサービスワーカー JavaScript を実装し、そのメッセージハンドラ内でskipWaiting()
メソッドを呼び出せばよいでしょう。 -
"The next version is ready (新しいバージョンの準備ができました)" のようなメッセージと "Update now (今すぐ更新)" のようなボタンを表示するユーザーインターフェイスを実装します。そしてユーザがそのユーザーインターフェース上の "Update now" ボタンをクリックしたときにサービスワーカープロセスに特定のメッセージを送り、サービスワーカープロセスの
skipWaiting()
メソッドが実行されるようにします。 -
navigator.serviceWorker.register
メソッドの Promise コールバックをハンドルします。 -
上記 Promise コールバック内にて、"待機中 "のサービスワーカープロセス(つまり新しいバージョンのサービスワーカー)があるかチェックし、もしある場合は、次のバージョンの準備ができたことを前述のユーザーインターフェースを介してユーザーに通知します。
-
続けて、サービスワーカー登録オブジェクトの
updatefound
イベントを購読します。updatefound
イベントが発生したら、さらに続けて、今度はサービスワーカープロセスのstatechange
イベントを購読します。 -
サービスワーカープロセスの
statechange
イベントが発生したら、そのサービスワーカープロセスの現在のステータスをチェックします。もし、ステータスが "installed" であれば、次のバージョンの準備ができたことを意味するので、その旨を前述のユーザーインターフェースでユーザーに通知します。 -
ステータスが "activated" であれば、ユーザが前述のユーザーインターフェースに応答し "Update now (今すぐ更新)" ボタンをクリックした結果として、
skipWaiting()
メソッドが実行されたことを意味するので、window.location.reload()
メソッドを呼び出して現在のページを再読み込みします。
...どうでしょう、この説明についてこれていますか?😅
🎉 "Blazor PWA Updater" NuGet パッケージとコンポーネント
幸い、上記のコードをすべて自分で実装する必要はありません! 代わりに、"Blazor PWA Updater " NuGet パッケージとそのコンポーネントを使用することができます。
-
Toolbelt.Blazor.PWA.Updater
NuGet パッケージをインストールします。 -
Blazor PWA の起動時処理にて、サービスプロバイダに「PWA updater」サービスを登録します。
-
- Blazor PWA 内の適当なコンポーネント内 (
App.razor
とかMainLayout.razor
とか) に<PWAUpdater>
コンポーネントを配置します。
- Blazor PWA 内の適当なコンポーネント内 (
-
service-worker.published.js
ファイルに"SKIP_WAITING"
メッセージハンドラを追加し、そのメッセージハンドラ内でskipWaiting()
メソッドを呼び出すように実装します。 -
index.html
内のスクリプト要素<script>navigator.serviceWorker.register('service-worker.js');</script>
を、<script src="_content/Toolbelt.Blazor.PWA.Updater.Service/script.min.js"></script>
に書き換えます。
以上です。
上記の実装を行うと、Blazor PWA 上で、すぐに新しいバージョンに更新するユーザーインターフェースとその機能が得られます。
また、通知テキスト、ボタンキャプション、テキスト色、背景色、サイズ、位置などといった、<PWAUpdater>
の外観をカスタマイズすることもできます。
さらには、<PWAUpdater>
の外観や動作が全く好みでない場合は、最小限の労力でゼロからユーザーインターフェースを実装することも可能です。NuGet パッケージの Toolbelt.Blazor.PWA.Updater.Service
に含まれる IPWAUpdaterService
サービスは NextVersionIsWaiting
イベントと SkipWaitingAsync()
メソッドを公開しており、DI からこのサービスを入手することで、残るユーザーインターフェイスを自分で好きなように構築することができます。
詳しくは「Blazor PWA Updater」の GitHub リポジトリにアクセスし、README を参照ください。
⚠️注意
なお、このような更新通知のユーザーインターフェースが頻繁に表示されると、ユーザーが煩わしく感じることもある点を心に留めておいてください。
もし、新しいバージョンをすぐに更新するような通知ユーザーインターフェイスを実装する十分な理由がないのであれば、デフォルトの実装のままにしておくのがよいでしょう。
Happy Coding :)