🐷

.NET 8 の Blazor で WASM と Server を完全に切り替える方法

2023/11/23に公開1

はじめに

.NET 8 の Blazor で WASM と Server と SSR をページ単位・コンポーネント単位で切り替えることが出来るようになりました。しかし、これはあくまでページの中だけの話です。そのまわりのレイアウト部分は単なる SSR でレンダリングされます。レイアウト部分まで WASM や Server にしてしまうと、グローバルで WASM なのか Server なのか固定化されてしまいます。

これをレイアウト部分まで Server と WASM を切り替えることをやってみようと思います。

やり方

やりかたは至って簡単です。レイアウトはアプリの Router が、どのモードでレンダリングされるかによって変わります。そして、コンポーネントは基本的に一度レンダリングされたらレンダリングのモードは固定になります。Auto を設定した場合もコンポーネントの初回レンダリング時に Server か WASM が決定されるだけで、後から変更することはできません。

なので WASM の Router と Server の Router (もしくは普通の SSR) を用意して、WASM と Server を切り替えるときは基本的にページ全体のリフレッシュで対応しようという方針です。

やってみよう

まずは Interactive render mode を Auto、Interactivity location を Per page/component にしてプロジェクトを作成します。

そして MainLayout.razorNavMenu.razor を WebAssembly のプロジェクトに移動します。今回は、レイアウトも WASM 側で動かすケースがあるので移動させることが必須です。忘れないように。

WASM 側は WASM 側で別のレイアウトを用意する場合は、WASM 側に新たにレイアウトファイルを作っても良いです。

そして WebAssembly 側のプロジェクトに WasmRouter.razor というファイルを作って以下のようにします。

WasmRouter.razor
@using BlazorApp41.Client.Layout
@using BlazorApp41.Client.Pages
@rendermode InteractiveWebAssembly

<Router AppAssembly="@typeof(Counter).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>

そして Server 側のプロジェクトにある Router.razor を変更して、WASM 側のページのパスの場合には WasmRouter を表示して、それ以外の場合は、元々ある Router を表示するようにします。

Router.razor
@using System.Diagnostics.CodeAnalysis
@using BlazorApp41.Client.Layout
@using BlazorApp41.Client.Pages
@inject NavigationManager NavigationManager

@if(IsWasmPath())
{
    <WasmRouter />
}
else
{
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
    </Router>
}

@code {
    private bool IsWasmPath()
    {
        var path = $"/{NavigationManager.Uri.Replace(NavigationManager.BaseUri, "")}";
        // ここの判定ロジックもうちょっとどうにかしたい…
        return path.StartsWith("/counter", StringComparison.InvariantCultureIgnoreCase);
    }
}

これで Counter.razor 以外は通常の SSR か Server で動いて、Counter.razor だけレイアウトの部分から WASM で動くようになります。

変更後のプロジェクトレイアウトは以下のようになります。

この状態だと Counter コンポーネント 1 つだけなのと、どっちのモードで動いているかわからないので Counter.razor を以下のように変更してパラメーターでカウンターの値を設定できるようにしてみました。

Counter.razor
@page "/counter/{count:int?}"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [Parameter]
    public int Count { get; set; }

    private int currentCount = 0;

    protected override void OnParametersSet()
    {
        currentCount = Count;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

そして MainLayout.razorNavMenu.razor を以下のように変更して、WASM で動いているのかどうかというのがわかるように WASM か SSR という文字列を表示するようにしてみました。

MainLayout.razor
@using System.Runtime.InteropServices
@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <article class="content px-4">
            <div>@(OperatingSystem.IsBrowser() ? "WASM" : "SSR")</div>
            @Body
        </article>
    </main>
</div>

<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

NavMenu.razor も同様に WASM と SSR を表示するようにするのと、Counter へのリンクを初期値無しと、初期値 1000 の 2 種類を用意してみました。

NavMenu.razor
<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">
            BlazorApp41(@(OperatingSystem.IsBrowser() ? "WASM" : "SSR"))
        </a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter" Match="NavLinkMatch.All">
                <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter/1000" Match="NavLinkMatch.All">
                <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter (1000)
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>
    </nav>
</div>

実行して動作確認をしてみましょう。

まず、初期ページは普通に表示されます。SSR と出ているので普通にサーバーサイドでレンダリングされています。

Counter へ遷移すると以下のような動作になります。とりあえずデフォルトの InteractiveWebAssembly を指定しているのでプリレンダリングがサーバーサイドで実行されているので最初は SSR と表示されます。

この状態だとボタンを押しても何も起きません。裏で WASM で .NET のランタイムが起動して動きだすと以下のように WASM に置き換わります。ページ全体が変わっていることが確認できます。このとき画面全体のちらつきなども発生しません。

この状態で Counter (1000) のリンクを選択すると完全な WASM 内での遷移になります。Router コンポーネントのレベルから WASM 内で動いているので、このような動きになります。

そして、Counter のページから離れると SSR に戻ります。

ちゃんと動いてますね。

プリレンダリングを切ると…

プリレンダリングを有効にすると WASM の場合でも一度サーバーサイドでレンダリングされます。そのためサーバーでもWASMでもちゃんと動くように作らないといけません。

これは結構大変なのですが、プリレンダリングをオフにすると SSR の所から WASM に行く際に Blazor WASM が立ち会がるまでの間ページが真っ白になります…。

これの解決方法は、ぱっとはわかりませんでした。わかったら何か書こうと思います。

WASM のページかどうかの判断方法

今回のサンプルでは Counter ページ決め打ちで WASM としていましたが、現実的には WASM のページには /wasm/counter のようにパスの先頭に /wasm などの文字列を付けて判断するのが良いと思います。

まとめ

ということで、普通のプロジェクト テンプレートだと、あくまでページのコンテンツ部分だけ SSR/Server/WASM を切り替え可能になり、レイアウト部分などは SSR 固定になってしまうという問題を解決する方法を試してみました。

多分、変な使い方をしてないと思うので大丈夫だと思います。

Microsoft (有志)

Discussion

なす38円なす38円

いつも突っ込んだ記事ありがとうございます!

>これをレイアウト部分まで Server と WASM を切り替えることをやってみようと思います。
すみません、この意図がよくわかっておりません。これが必要となるシナリオに思い至っていないというか;Wasmのリロードとは関係ないトピックですよね?

Client側のコードはメモリーリークが怖いというか、長い間使っていると動作が重くなってくることがあるので、しかたなくリロードが必要になる場合があります。今度のBlazorはシームレスにServerとClient側でコードが切り替わるので、Wasmがいつロードされるの?がよく理解できていません;レイアウトを分ける/分けないに関わらず、Home→Counter時にWasmがリロードされると思ってよいのでしょうか?