Blazor Server でサーバーとの接続障害からの復旧後、「再読込」をユーザに押させることなく自動でページ再読み込みする

# Blazor Server Web アプリケーションは常時接続が必須・途切れると操作がロックされる

Blazor Server で実装された Web アプリケーションは、その実行の仕組み上、ブラウザ - Webサーバ間の常時接続 (大抵の場合は WebSocket による) が必須です。

一時的な回線接続不良などでこの常時接続が途切れると、ブラウザの表示上、白いマスクでページ全体が覆われ、「再接続を試行中...」のプログレス表示がぐるぐる表示されつつ、接続が復旧するまで操作不能にロックされます。
![blazor-server-auto-reload - attempting-to-reconnect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/5b7bf868-6edd-d989-5a9c-3d3adf0a7450.png)
このあと程なくして回線が復旧し再接続できれば、この白いマスク表示は消えて、引き続きこの Blazor Server Web アプリをそのまま使用続行できます。

しかしながら、所定の再試行回数を超えても接続が復旧しなかったり、あるいは Web サーバープロセスの再起動などで Blazor Server のセッションが消失してしまった場合は、接続が復旧しなかった旨、白いマスク上に表示され、「再読込」のボタンが表示されます。
![blazor-server-auto-reload - could-not-reconnect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/d7d270f0-fe22-f823-a053-3265a335031e.png)

この「再読込」ボタンをユーザーがクリックすれば、そしてその時点で、回線や Web サーバが復旧していれば、Web サーバ再起動時などは先ほどまでの操作状態は失われますものの、とりあえずはその時点の URL のページが表示されて、Blazor Server Web アプリケーションとして改めて操作可能となります。

繰り返しになりますが、Blazor Server はその仕組みの都合から、どうしても常時接続が切れてしまった場合にはユーザー操作不能とせざるを得ません。
残念ながら、少なくとも今日現在では、こればっかりは仕方ありません。

# だったら自動でページリロードしてしまってもよいのでは?

しかしです。

白いマスク表示の上に「再読込」ボタンを表示して、それを**わざわざユーザーに押させる**、というのは、他にもやりようがあるのではないだろうか、と思われました。

すなわち、どうせもう、ページ再読込する以外**選択肢がない**のであれば、それをいちいちユーザーに操作させず、回線やサーバーが復旧次第、**強制的に自動でページ再読込**させてしまうことでもよいのではないでしょうか。
(※もちろん、ケースバイケースでしょうし、それらを考慮して、現状の Blazor Server の振る舞い・仕様は前述のとおりとなっているのであろうと理解しております)

ということで、

「サーバーとの接続障害からの復旧後、[再読込] をユーザに押させることなく自動でページ再読み込みする」

よう、Blazor Server Web アプリケーションを構成・実装する方法を調べてみました。

幸いなことに、これら Blazor Server の接続関連の情報は、下記公式ドキュメントサイトに掲載があります。

https://docs.microsoft.com/ja-jp/aspnet/core/blazor/fundamentals/signalr?view=aspnetcore-6.0&pivots=server

上記ページを見ると、どうやら、`Blazor.defaultReconnectionHandler` という JavaScript 変数をごにょごにょするとどうにか出来そうに読めます。

ただ、既存の・既定の実装を拡張するというのではなく、全部自前の実装に置き換える仕組みのように読めました。
すなわち、この路線でやれなくはないけれども、**Blazor Server の既定実装と同じ振る舞いを、かなりモリモリと自前で再実装しなければならなそう**な予感がしました。
そうなると、動作確認も大変でしょうし、それはちょっとイヤです。

そこで、少々汚いハックかなとも思いましたが、`Blazor.defaultReconnectionHandler` に頼らない方法で実装することにしました。

以下で、その方法を紹介したいと思います。

#「再読込」をユーザに押させることなく自動でページ再読み込みするよう実装する手順

## 「再接続中」の表示をカスタマイズする

まずは「再接続中」の白いマスク表示 = モーダル表示をカスタマイズし、「x 回目の再接続中」とかいうのではなく、ただただ一言「再接続中」とだけ表示するようにしてみます。

上記にリンク先紹介した公式ドキュメント中、["UI に接続状態を反映する"](https://docs.microsoft.com/ja-jp/aspnet/core/blazor/fundamentals/signalr?view=aspnetcore-6.0&pivots=server#reflect-the-connection-state-in-the-ui) のセクションに説明があるとおり、"`components-reconnect-modal` の id を持つ要素を定義" することで、これが実現できます。

ということで、自分は下記のとおり HTML を記述しました。

```html:Pages/_Layout.cshtml
...
<div id="components-reconnect-modal">
    <div class="loading">
        <div class="loading-caption">再接続中...</div>
        <div class="loading-progress-ring"></div>
    </div>
</div>
...
```

そして、先の公式ドキュメントに記載のあるとおり、この `components-reconnect-modal` の id を持つ要素に対して、Blazor Server の接続状態に応じた CSS クラス名が、Blazor Server のランタイムによって付加されます。

それを加味して、上記自前の「再接続中」モーダル表示 HTML 要素の外観や可視状態を CSS で定義します (下記)。

```css:wwwroot/css/components-reconect-modal.css
#components-reconnect-modal {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    z-index: 1000;
    background-color: rgba(255, 255, 255, 0.7);
    /* 👇 初期状態では、「再接続中」モーダルは不可視 */
    display: none;
}

    #components-reconnect-modal.components-reconnect-show,
    #components-reconnect-modal.components-reconnect-failed,
    #components-reconnect-modal.components-reconnect-rejected {
        /* 接続状態に何かしら問題発生している場合は👆のセレクタで示されるような
           CSS クラス名が付くのでそのときは「再接続中」モーダルを可視化 👇 */
        display: block;
    }

    /* 👇 ここから下は、「再接続中」モーダルにおけるスピナーのアニメーション表示
          など、外観の調整・定義  */
    #components-reconnect-modal .loading {
        width: 360px;
        height: 110px;
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        margin: auto;
    }

        #components-reconnect-modal .loading .loading-caption {
            color: #88a;
            font-size: 18px;
            text-align: center;
        }

        #components-reconnect-modal .loading .loading-progress-ring {
            border: solid 10px;
            border-color: #88a #eee #eee;
            border-radius: 50px;
            position: absolute;
            width: 50px;
            height: 50px;
            top: unset;
            bottom: 0;
            left: 0;
            right: 0;
            margin: auto;
            animation: rotation 1.5s linear 0s infinite;
        }

@keyframes rotation {
    0% {
        transform: rotate(45deg);
    }

    100% {
        transform: rotate(405deg);
    }
}
```

CSS は少し大きくなってしまいましたが、本質的には大したことはやっていません。

## 接続試行回数を (ほぼ) 無限回にする

続けて、既定では8回の接続再試行でそれでも接続復旧しない場合は断念して「再読込」ボタンが表示されてしまう挙動について手を入れます。

すなわち、接続再試行回数を (実質上の) "無限回" に変更することで、つまり、サーバーとの通信が可能になるまで**決して "断念" しない**ようにすることで、**そもそも「再読込」ボタンが表示されるような状況を作らない**、という作戦ですw 😁

この点については、先にリンク紹介した公式ドキュメント中 ["再接続の再試行回数と間隔を調整する"](https://docs.microsoft.com/ja-jp/aspnet/core/blazor/fundamentals/signalr?view=aspnetcore-6.0&pivots=server#adjust-the-reconnection-retry-count-and-interval) のセクションに丁寧に記載があるとおり、Blazor Server アプリケーションにおける**接続再試行回数は設定可能**です。

やってみましょう。

まずは、Blazor Server アプリケーションが既定の設定で自動開始しないよう、自動開始の抑止を指定します。
これは、`_framework/blazor.server.js` を読み込んでいるフォールバック HTML を実装しているページ (既定のプロジェクトテンプレートからの生成だと、.NET 6 では、`Pages/_Layout.cshtml` になります) にて、読み込んでいる `<script>` タグに、`autostartt="false"` という属性を書き足します。

```html:Pages/_Layout.cshtml
...
<script src="_framework/blazor.server.js" autostart="false"></script>
             <!-- autostart="false" を書き足す 👆 -->
...
```

続いては、接続再試行回数のオプションを指定して Blazor Server アプリケーションを明示的に開始する JavaScript コードを実装します。
(※残念ながら (?)、ここは C# ではなく JavaScript で書くしか道はありません)

実装する JavaScript コードの記載先は何でもよいのですが、ここでは `boot.custom.js` というファイル名の JavaScript ファイルに実装することにします。

どう実装すればよいのかの一次情報は先に紹介の公式ドキュメントを参照いただければわかりますが、下記のように実装することで、接続再試行回数のオプションを指定して Blazor Server アプリケーションを明示的に開始することができます。

```js:boot.custom.js
﻿"use strict";

Blazor.start({
    // 再試行のペースは2秒に1回とし、再試行回数は
    // これを1年間継続するのに相当する回数 👇 を指定してみました。
    reconnectionOptions: { maxRetries: 15768000, retryIntervalMilliseconds: 2000 },
});
```

本節冒頭では "無限回" と書きましたが、実際には無限ではなく有限で、ただ、「1年間、再試行し続ける」という再試行回数を指定しています。
まったく無限ではないんですけれども、まぁ、1年間も本当にブラウザ立ち上げて再接続を待つことは極めて考えにくいので、これくらいでいいですよね。

こうして作成した `boot.custom.js` JavaScript ファイルを、ブラウザに読み込まれるよう、以下のように `_framework/blazor.server.js` のあとに `<script>` タグを書き足します。

```html:Pages/_Layout.cshtml
...
<script src="_framework/blazor.server.js" autostart="false"></script>

<!-- 👇 blazor.server.js のあとに、boot.custom.js の読み込みを書き足す -->
<script src="boot.custom.js"></script>
...
```

以上で、実質上、回線およびサーバーが復旧するまで、延々と再試行し続ける Blazor Server アプリケーションになりました。

## 同一セッションに再接続できない場合にページ再読込を発動する

さて、回線およびサーバーが復旧するも、サーバープロセスが再起動したなどでセッション情報が揮発してしまい、「接続拒否」になってしまう場合もあります。

この場合は現在の URL でページ再読込 (`window.location.reload()`) を発動するようにしてみます。

さてところで、既に述べたとおり、今回は `Blazor.defaultReconnectionHandler` 等をひねくり回す作戦は放棄しています。

わかっているのは、「接続拒否」が発生すると、先にカスタマイズした「再接続中」モーダル表示の HTML 要素に、**`components-reconnect-rejected` という CSS クラスが付加される**、ということです。

そこで、HTML 要素の "変化" を "監視" するブラウザの仕組み、**"MutationObserver"** を利用することにしました。

つまり、`MutationObserver` を使うことで、「再接続中」モーダル表示の HTML 要素に何かしら CSS クラスが付加されたら、自前の JavaScript コードを呼び出してもらう、そんな JavaScript プログラムが実現できる、というわけです。
(※残念ながら (?)、ここでも引き続き C# ではなく JavaScript で書くしか道はありません 😅)

`MutationObserver` の詳細については下記 MDN のリンク先を参照いただくのがよいでしょう。

https://developer.mozilla.org/ja/docs/Web/API/MutationObserver

取り急ぎ、今回の目的のために、以下のように `boot.custom.js` に JavaScript コードを書き足していくことにします。

まずは、以下のように `MutationObserver` のインスタンスを構築します。
`MutationObserver` のコンストラクタ引数には、監視対象の HTML 要素に変化発生したときに呼び出されるコールバック関数を指定します。

このコールバック関数内で、変化発生した監視対象の HTML (のちほど、この監視対象には、「再接続中」モーダル表示の HTML 要素を指定します) の CSS クラスを調べ、もし `components-reconnect-rejected` という CSS クラス名が見つかったなら、ブラウザのページ再読込を実行する、というように実装します。

```js:boot.custom.jsの冒頭
const observer = new MutationObserver(mutations => {
    // 👇 監視対象の HTML に "変化" があると、ここが実行される
    mutations.forEach(mutation => {
        // 👇 変化があった、監視対象 HTML 要素の CSS クラスを調べ...
        const classList = mutation.target.classList;
        // 👇 `components-reconnect-rejected` という CSS クラス名が見つかったなら、
        if (classList.contains('components-reconnect-rejected') === true) {
            // 👇 ブラウザのページ再読込を実行!
            window.location.reload();
        }
    });
});
```

こうして「変化を検出したらどうするか」を指定して `MutationObserver` をインスタンス化できたら、あとは「どの HTML 要素を監視するか」を指定して、監視を開始します。

監視対象の HTML 要素、すなわち今回は「再接続中」モーダル表示の要素を `document.getElementById()` で取得し、先のとおりインスタンス化した `MutationObserver` の `observe()` メソッドに、他のオプション指定と合せて引き渡すことで、監視が始まります。

```js:boot.custom.jsの続き
...
// 👇「再接続中」モーダル表示の HTML 要素を取得
const reconnectModal = document.getElementById('components-reconnect-modal');

if (reconnectModal !== null) {
    // その HTML 要素 👇 を、先ほど初期化した MutationObserver インスタンスで監視開始!
    observer.observe(reconnectModal, { attributes: true, subtree: false });
}
```

以上で、いったんの常時接続断から復旧するも元のセッションに再接続できず接続拒否になった場合、すなわち、「再接続中」モーダル要素に `components-reconnect-rejected` CSS クラスが付加されたタイミングで、ブラウザのページ再読込 `window.location.reload()` が実行されるようになりました。

## 完成!

これで全ての実装作業が完了です!

Blazor Server アプリケーション開発において、このような作り込みをすることで、もしもの常時接続が切断後、回線やサーバーが復旧するまで延々「再接続中」を表示継続しつつ、ひとたび復旧すれば、ユーザーにわざわざ「再読込」ボタンを押させることなく、最悪の場合はそれまでの操作状態は失われますが、とにかくユーザー操作不要で再びアプリが操作可能になる、というユーザー体験を実現することができます。

および、今回紹介の方式の実装コードすべてを、下記 GitHub リポジトリで公開しました。
変更履歴を見て頂ければ、実装手順ごとの差分も見ることができます。

https://github.com/sample-by-jsakamoto/Blazor-Server-AutoReload

なお、`Blazor.defaultReconnectionHandler` をはじめとした公開 API を使うことで、今回紹介の方式と同等のユーザー体験を実装できるのかどうか、自分は未だにわかっておりません。

もし何か情報お持ちの方がいらっしゃれば、本投稿のコメントなどで教えて頂けると、自分を含め、助けられる方が少なからずいるかと思います、ぜひお知らせいただければと思います。

# セッション喪失・接続拒否をそもそも回避できないのか?

今回記事の主旨から若干外れるので、今回投稿では深追いしませんが、例えばサーバー再起動に伴うセッション喪失・接続拒否について、少し補足します。

そもそもサーバー再起動ごときでセッション喪失するのが設計的におかしいのかもしれません。
とはいえども、あいにくと自分は、Blazor Server における個々の接続セッションをサーバー再起動しても維持できるのかどうか、そこのところをよくわかってはいません。

しかしです、本当に重要な操作状態は、**ブラウザのセッションストレージやローカルストレージ、あるいは URL のフラグメントやクエリ文字列に保存し、復元**するようプログラムを作り込みできるわけです。

別の言い方をすると、常時接続が云々とは関係なしに、**ユーザが意図的にブラウザの再読込ボタンをクリック**したとしても、**必要最低限のそれまでの操作状態が維持・復元**されるように、そうなるように Blazor Server アプリを実装しておけばよい、ということになります。

今回紹介の技法に加えて上記のように作り込まれてさえいれば、サーバー再起動したとしても、**ユーザーによる操作不要**で、かつ、**それまでの操作状態を失うことなく**、**自動で再び操作可能になる**、というユーザー体験を提供できます。

# おわりに

繰り返しになりますが、このような振る舞いが、すべての Blazor Server Web アプリケーションにおいて最適とは限りません。
その Web アプリケーションの対象ユーザーや使用場面、各種要件次第です。

例えば、同時使用ユーザー数が大規模な Blazor Server アプリで、今回紹介の実装をまんまコピペすると、万が一の障害の際、すべてのクライアントが2秒に1回接続試行してくるという、**障害で大変なところ更に追い打ちをかけるように大量リクエストが着信する**ような羽目にもなります。

ですが、うまく今回紹介の方式がフィットする案件であれば、例えば

「なんか画面が白くなったまま止ってるんですけど、どうしたらいいですか?」
(実はこの時点で復旧済みだが、画面は「再読込」ボタンが表示されていてユーザー操作を待っている状態だった)

といったような**問い合わせの件数を 0 にすることもできる**わけです。

あるいはまた、ユーザー入力デバイスを常時は備えていないような表示専用の機材で Blazor Server Web アプリによるダッシュボードを常時表示しているようなケースで、

「サーバが再起動したとしても手放しで表示復旧させたい、つか、**普段は人が画面操作できないw**」

といった状況・需要に対しても、今回紹介の方式が活用できることでしょう。

以上、今回紹介の方式が何かの案件に適用・役立つことがあるといいですね。

_Learn, Practice, Share!_