koudenpaのブログ

趣味のブログです。株式会社はてなでWebアプリケーションエンジニアをやっています。職業柄IT関連の記事が多いと思います。

Blazor WebAssembly のSSRを試した

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のクライアントサイドとサーバサイドレンダリングと比べてどちらが地獄かは分からない。意外といけるんじゃないか? って気になってきている。