Blazor的にはSSRではなくプリレンダリング(Prerendering)と呼ぶらしいけれど、やりたいこと的にはサーバサイドレンダリングの方が通りがいいのではないかと思う。
どうすれば Blazor WebAssembly をプリレンダリングできるのか
プリレンダリングするには前提としてサイトをASP.NET Coreでホスティングする必要がある。この辺りに案内がある。
- Host and deploy ASP.NET Core Blazor WebAssembly | Microsoft Docs
- Use ASP.NET Core SignalR with Blazor WebAssembly | Microsoft Docs
- Blazor Server同様にSignalRを使うために、という立て付けのドキュメントだがこちらも分かりやすい。
その上で、アプリケーションのエントリポイントを静的な index.html
として配信するのではなく、動的にRazorテンプレートをレンダリングするように構成する。
- ASP.NET Core Blazor WebAssembly additional security scenarios | Microsoft Docs
- 認証+プリレンダリングという立て付けだけれどどうすればいいのかまとまっていて分かりやすい
- GitHub - danroth27/BlazorWebAssemblyWithPrerendering: Client-side Blazor app with prerendering
- プリレンダリング機能が実装されたころのサンプルソリューション。最小構成で分かりやすい。
ここで注意が必要なのは、このプリレンダリングはASP.NET Coreのサーバサイドアプリケーションで行われる点だ。
実行環境がWebAssemblyではなく.NET Coreなので、サポートしている機能は異なっている。加えて、Webブラウザ上での実行ではないためJavaScriptの相互運用機能は使用できない(できないよと例外する)。
これを動作させるにはRazorテンプレートは.NET Standardに依存する形で記述し、クライアントサイドとサーバサイドでそれぞれに適した実装をDIしてやればよい。
余談だけれど、どうすればSSRできるのかちょっととっつきが悪かった。そういったIssue(Document prerendering in Blazor (WebAssembly) · Issue #11366 · dotnet/AspNetCore.Docs · GitHub)は立っていた。『そもそもプリレンダリングについて分かりづらいんじゃないの?』と『WebAssembly のサーバサイドレンダリングしたい』の2トピックがある感じだろうか。
こんな感じに実装してみた
先日作ったこれを WebAssembly してプリレンダリングした。
Blazor Serverでのホスティングも生きているので、Blazor Serverでのホスティング、Blazor WebAssembly のクライアントサイドとサーバサイドで3通りのRazorコンポーネントレンダリングをしていることになる。Blazor楽しいじゃないか。
App Serviceでホスティングしている。
基本構成はコピペ
ASP.NET Core Blazor WebAssembly additional security scenarios | Microsoft Docs から。
プリレンダリングの起点となるテンプレートはこれ。MultiComputerVision/_Host.cshtml at 43c61baf77936b207f5ee413e6fe0a70e5ae1d39 · 7474/MultiComputerVision · GitHub
<app> @if (!HttpContext.Request.Path.StartsWithSegments("/authentication")) { <component type="typeof(App)" render-mode="Static" /> } else { <text>Loading...</text> } </app>
(認証のないページは存在しないので)この if 文意味ないな。後で消そう。
Razorテンプレートは.NET Standardに依存する形で記述
まず、必要な処理のインタフェースを定義。 MultiComputerVision/IResultDocumentService.cs at 43c61baf77936b207f5ee413e6fe0a70e5ae1d39 · 7474/MultiComputerVision · GitHub
public interface IResultDocumentService { Task<IResultDocument> GetResult(Guid id); Task<IList<IResultDocument>> GetResults(DateTimeOffset offset); }
テンプレートではインタフェースを依存関係として注入(DI)する。 MultiComputerVision/Result.razor at 43c61baf77936b207f5ee413e6fe0a70e5ae1d39 · 7474/MultiComputerVision · GitHub
@page "/results/{id}" @inject IResultDocumentService ResultDocumentService @if (doc == null) { <p><em>Loading...</em></p> } else { <div> <img src="@doc.Image.Uri" style="max-width: 100%" /> </div> } @code { doc = await ResultDocumentService.GetResult(Guid.Parse(Id)); title = doc.GetTitle(); description = doc.GetDescription(); }
※コードは適当に抜粋している
クライアントサイドとサーバサイドでそれぞれに適した実装をDI
サーバサイドでは適当に処理する。クライアントサイドで動作しないので何でもやり放題だ。 MultiComputerVision/ServerSideResultDocumentService.cs at 43c61baf77936b207f5ee413e6fe0a70e5ae1d39 · 7474/MultiComputerVision · GitHub
public class ServerSideResultDocumentService : IResultDocumentService { private readonly IResultRepositoryService resultRepositoryService; public ServerSideResultDocumentService(IResultRepositoryService resultRepositoryService) { this.resultRepositoryService = resultRepositoryService; } public async Task<IResultDocument> GetResult(Guid id) { return await resultRepositoryService.GetResult(id); }
public void ConfigureServices(IServiceCollection services) { // ~略~ services.AddSingleton<IResultDocumentService, ServerSideResultDocumentService>(); services.AddSingleton<IUploadImageService, ServerSideUploadImageService>();
クライアントサイドではサーバーが提供(自分で作ったわけだけれど)しているAPIへのリクエストとしてインタフェースを実装する。 MultiComputerVision/ResultDocumentService.cs at 43c61baf77936b207f5ee413e6fe0a70e5ae1d39 · 7474/MultiComputerVision · GitHub
public class ResultDocumentService : IResultDocumentService { private readonly HttpClient httpClient; public ResultDocumentService(AllowGuestHttpClient httpClient) { this.httpClient = httpClient; } public async Task<IResultDocument> GetResult(Guid id) { return await httpClient.GetFromJsonAsync<PlainResultDocument>($"Api/ResultDocument/{id}"); }
public static async Task Main(string[] args) { // ~略~ builder.Services.AddSingleton<IResultDocumentService, ResultDocumentService>(); builder.Services.AddSingleton<IUploadImageService, UploadImageService>();
サーバサイドでは.NET Coreでプリレンダリングされて、その後クライアントサイドではWebAssemblyで動作する複合的なアプリケーションとなった。実にカオス。
感想
「これこそが依存関係の注入だよ!」って感じでテンションが上がった。趣味で試す分にはよい。
大き目のプロジェクトで開発・運用するのは地獄を見そうだが、JavaScriptのクライアントサイドとサーバサイドレンダリングと比べてどちらが地獄かは分からない。意外といけるんじゃないか? って気になってきている。