Blazor的にはSSRではなくプリレンダリング(Prerendering)と呼ぶらしいけれど、やりたいこと的にはサーバサイドレンダリングの方が通りがいいのではないかと思う。
どうすれば Blazor WebAssembly をプリレンダリングできるのか
プリレンダリングするには前提としてサイトをASP.NET Coreでホスティングする必要がある。この辺りに案内がある。
その上で、アプリケーションのエントリポイントを静的な index.html
として配信するのではなく、動的にRazorテンプレートをレンダリングするように構成する。
ここで注意が必要なのは、このプリレンダリングは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 してプリレンダリングした。
koudenpa.hatenablog.com
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);
}
で、それをDI。MultiComputerVision/Startup.cs at 43c61baf77936b207f5ee413e6fe0a70e5ae1d39 · 7474/MultiComputerVision · GitHub
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}");
}
で、それをDI。MultiComputerVision/Program.cs at 43c61baf77936b207f5ee413e6fe0a70e5ae1d39 · 7474/MultiComputerVision · GitHub
public static async Task Main(string[] args)
{
builder.Services.AddSingleton<IResultDocumentService, ResultDocumentService>();
builder.Services.AddSingleton<IUploadImageService, UploadImageService>();
サーバサイドでは.NET Coreでプリレンダリングされて、その後クライアントサイドではWebAssemblyで動作する複合的なアプリケーションとなった。実にカオス。
感想
「これこそが依存関係の注入だよ!」って感じでテンションが上がった。趣味で試す分にはよい。
大き目のプロジェクトで開発・運用するのは地獄を見そうだが、JavaScriptのクライアントサイドとサーバサイドレンダリングと比べてどちらが地獄かは分からない。意外といけるんじゃないか? って気になってきている。