.NET 8 の Blazor で WASM と Server を完全に切り替える方法
はじめに
.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.razor
と NavMenu.razor
を WebAssembly のプロジェクトに移動します。今回は、レイアウトも WASM 側で動かすケースがあるので移動させることが必須です。忘れないように。
WASM 側は WASM 側で別のレイアウトを用意する場合は、WASM 側に新たにレイアウトファイルを作っても良いです。
そして WebAssembly
側のプロジェクトに 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
を表示するようにします。
@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
を以下のように変更してパラメーターでカウンターの値を設定できるようにしてみました。
@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.razor
と NavMenu.razor
を以下のように変更して、WASM で動いているのかどうかというのがわかるように WASM か SSR という文字列を表示するようにしてみました。
@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 種類を用意してみました。
<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 固定になってしまうという問題を解決する方法を試してみました。
多分、変な使い方をしてないと思うので大丈夫だと思います。
Discussion
いつも突っ込んだ記事ありがとうございます!
>これをレイアウト部分まで Server と WASM を切り替えることをやってみようと思います。
すみません、この意図がよくわかっておりません。これが必要となるシナリオに思い至っていないというか;Wasmのリロードとは関係ないトピックですよね?
Client側のコードはメモリーリークが怖いというか、長い間使っていると動作が重くなってくることがあるので、しかたなくリロードが必要になる場合があります。今度のBlazorはシームレスにServerとClient側でコードが切り替わるので、Wasmがいつロードされるの?がよく理解できていません;レイアウトを分ける/分けないに関わらず、Home→Counter時にWasmがリロードされると思ってよいのでしょうか?