.NET で SPA が書ける Blazor の使い方をざっくり解説

Blazor というのは、SPA のクライアントアプリを .NET で書くと、WebAssembly 上で .NET のバイナリのまま動かすことができるというフレームワークだ。

一見かなりアクロバティックな仕組みだし、.NET 界隈だと Silverlight を思い出して「うっ頭が…」となる人もいるようだが、まあ今回は独自プラグインではなく、Web 標準 である WebAssembly にちゃんと乗ってるところが大きな違い。

完成度としては、「実験的なプロジェクトなので Production では使わないこと」とうたってはいるが、実際使ってみるとすでに基本的な機能はかなりできあがっていてそこそこちゃんと動くので、使い方をざっくりまとめておく。

※ちなみに Blazor という名前の由来は、公式 FAQ によると "Browser + Razor = Blazor" とのこと。なぜ二文字目がRじゃなくてLなのかは謎。

開発環境の構築

.NET Core 2.1 RC SDK のインストール

まずは、.NET Core 2.1 SDK の最新版をインストールする。2018年5月17日時点で最新バージョンは2.1.300-RC1。

www.microsoft.com

Visual Studio 2017 Preview のインストール

.NET Core 2.1 を使うには、Preview 版の Visual Studio が必要となる。以下のリンクからインストールする。

www.visualstudio.com

なお Preview 版は、通常版の Visual Studio 2017 と同居させられる作りになっている。通常版を利用している人も何も考えずにインストールしてしまって大丈夫。 同居させると、インストーラの画面はこんな感じになる。

f:id:nosunosu:20180518120526p:plain

テンプレートからプロジェクトを新規作成

プロジェクトの新規作成で、「ASP.NET Core Web アプリケーション」から、「Blazor (ASP.NET Core hosted)」を選択する。

今回は「Blazor (ASP.NET Core hosted)」を選択して、サーバサイドの Web API を含んだソリューションを作成するが、すでに Web API が別にあり、純粋にクライアントサイドのみ開発したい場合には、クライアントアプリのみのプロジェクトが生成される「Blazor」を選択する。

f:id:nosunosu:20180518120553p:plain

※「Blazor」を選択した場合、ビルドして生成されるのは単なる静的ファイル群なので、静的な Web サイトをホストできるサービスであればどこにでも配置して公開できる。一方「Blazor (ASP.NET Core hosted)」の場合は、当然ながら ASP.NET Core が動く環境でである必要がある。

なおいずれの場合も、ランタイムは ASP.NET Core 2.1 または 2.0 を選択すること。

プロジェクトを作成すると、以下のようなプロジェクト構成のソリューションが作成される。

f:id:nosunosu:20180518120649p:plain

  • [プロジェクト名].Client
  • [プロジェクト名].Server
  • [プロジェクト名].Shared

ほぼ自明ではあるが、それぞれ以下のような役割のプロジェクトとなる。

.Client

Blazor を使ったクライアントアプリの本体部分。 見た目上は、.cshtml ファイルが Pages フォルダ内に配置されていて、Razor Pages のプロジェクトっぽい(Blazor が Razor の拡張なので当たり前といえば当たり前)。

Blazor 用に以下の NuGet パッケージがインストールされている。 - Microsoft.AspNetCore.Blazor.Browser - Microsoft.AspNetCore.Blazor.Build

.Server

サーバサイドの Web API。 中身は普通の ASP.NET Core Web API なので、特に難しいところはない。

実行時には、.Client は、この .Server プロジェクトの ASP.NET Core 上にホスティングされる状態になる。 具体的には、.Server プロジェクトから .Client プロジェクトを参照し、さらに以下のように .Server プロジェクトの Startup クラス Configure() 内で app.UseBlazor<T>() を呼ぶことで、ホスティングする Blazor クライアントプロジェクトを指定している。

Startup.cs

public class Startup {
    public void ConfigureServices(IServiceCollection services) { ... }
    public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
        ...
        app.UseBlazor<Client.Program>();
    }
}

.Shared

クライアントとサーバ両方から参照されるクラスライブラリ。 主にはクライアントとサーバでやりとりする JSONシリアライズするためのクラスを配置するのに使用する。

とりあえず実行してみる

スタートアッププロジェクトは .Server を選択して、デバッグ実行する。 初期状態では、クライアント単独で動くカウンタと、Web API から取得した天気情報を表示するサンプルページが用意されている。

なお、デバッガはまだ開発中なので、Blazor のコンポーネント内にブレイクポイントを置いても動作しない。

ここまででセットアップは終わりなので、あとはアプリを書いていくだけだ。 以降では、Blazor アプリの書き方の基本を少し説明する。

Blazor アプリの書き方

公式の FAQ に、"inspired by existing modern single-page app frameworks, such as React, Angular, and Vue" とあるだけあって、React などの JavaScript の UI フレームワークの考え方がいろいろごっちゃになって取り入れられている。

基本的な使い方としては、C#/HTML からなるコンポーネントという塊を作り、それを入れ子にして組み合わせることでアプリケーションを構成する、というおなじみの仕組みになっている。

コンポーネントを作成するには、以下の3つのやり方がある。

  • インライン (.cshtml) 方式
  • コードビハインド方式
  • クラス方式

まずはとりあえず、一番わかりやすいインライン方式で説明する。

インライン方式でのコンポーネント作成

インライン方式では、一つの .cshtml ファイルが一つのコンポーネントとなる。 .cshtml ファイルなだけあって、従来の Razor 構文で View を記述すれば良いのだが、大きな違いとして、Blazor コンポーネントでは「コンポーネント変数」を持つことができる。

コンポーネント変数は、.cshtml の @functions{} 内で[Parameter] 属性を付けた変数として宣言する。 これにより、React における Props のように、コンポーネントを使う親コンポーネント側で属性値として値を渡すことができる。

また、コンポーネント名のタグに挟まれたテキストの値は、RenderFragment 型の ChildContent という名前のコンポーネント変数(名前は変更不可)を定義しておくことで、受け取ることができる。

Child.cshtml

<!-- 子コンポーネント -->
<h1>@Title</h1>
<p>@ChildContent</p>

@functions {
    [Parameter]
    private string Title { get; set; }

    [Parameter]
    private RenderFragment ChildContent { get; set; }
}

Parent.cshtml

<!-- 親コンポーネント -->
<Child Title="ここでタイトルを渡す">ここでChildContentを渡す</Child>

親から子には、Action を渡すこともできる。 これを使うと、React 風に、Presentational/Container でコンポーネントを分け、子コンポーネント(Presentational)で発生したイベントを親コンポーネント(Container)に伝えて、ロジックは親側に集約するようなことができる。 子コンポーネントでは、渡された Action を任意の契機で Invoke() すれば良い。

ただし、イベント契機で親が管理する値を変更した場合、手動で StateHasChanged() を叩かないと、画面上に変更は反映されないことに注意する。 非公式ドキュメントには、StateHasChanged() は親と子で独立に動くので、親と子両方に反映するには双方で呼ぶ必要があるという記載があるが、試した限りでは、親側でさえ呼べば、子側にも変更が反映された。

以下のサンプルでは、子側が持つ「Count!」ボタンを押下すると、親側の Action が実行されて currentCountOnParent がカウントアップされ、その変更が子側にも伝播することで、両方の表示が更新される。

コンポーネント

ParentComponent.cshtml

@page "/parentComponent"

<h1>親子での連携テスト</h1>
<p>Count on Parent: @currentCountOnParent</p>
<ChildComponent currentCountOnChild=@currentCountOnParent OnChildButtonClicked=@ChildButtonClicked />

@functions {
    private int currentCountOnParent = 0;

    private void ChildButtonClicked() {
        Console.WriteLine("ChildButton is clicked");
        currentCountOnParent++;

        // 手動で描画を更新する
        StateHasChanged();
    }
}

コンポーネント

ChildComponent.cshtml

<p>Count on Child: @currentCountOnChild</p>
<button onclick=@OnClick>Count!</button>

@functions {
    [Parameter] private int currentCountOnChild { get; set; }   
    [Parameter] private Action OnChildButtonClicked { get; set; }

    private void OnClick() {
        OnChildButtonClicked?.Invoke();
    }
}

インライン方式以外のコンポーネント作成方法

インライン方式以外での記述方法は以下のとおり。

コードビハインド方式

C# のロジック部分と HTML(Razor) を分けて記述する方式。 具体的には、C# 部分は BlazorComponent を継承するクラスに記述し、View 部分のみ .cshtml に記述した上で、@inherits 文を使ってロジックのクラスを指定する。

ChildBase.cs

public class ChildBase : BlazorComponent {
    [Parameter]
    private string Title { get; set; }

    [Parameter]
    private RenderFragment ChildContent { get; set; }
}

ChildByCodeBehind.cshtml

@inherits ChildBase

<h1>@Title</h1>
<p>@ChildContent</p>

クラス方式

純粋に C# のクラスとしてコンポーネントを作成することもできる。 実は、他の 2 方式で作ったコンポーネントも、プロジェクトをビルドすると [コンポーネント名].g.cs というファイル名でこの形式のクラスに自動的に変換されている。

クラス内の BuildRenderTree() で、HTML の要素を一つずつ組み立てていく必要があるため、必要がない限りあえて用いる必要はない。

ChildByClass.cs

public class ChildComponent : BlazorComponent {
    private string Title { get; set; }
    private RenderFragment ChildContent { get; set; }

    protected override void BuildRenderTree(RenderTreeBuilder builder) {
        builder.OpenElement(1, "h1");
        builder.AddContent(2, Title);
        builder.CloseElement();
        builder.OpenElement(3, "p");
        builder.AddContent(4, ChildContent);
        builder.CloseElement();
    }
}

ルーティング

作成したコンポーネントにアクセスするためには、ルーティングを定義してやる必要がある。

インライン方式・コードビハインド方式の場合は、.cshtml の先頭に @page "/path/to/component" という形式で宣言してあげるだけで良い。

Parent.cshtml

@page "/parent"
@page "/parent2" // 一つのコンポーネントに複数のパスを定義することもできる
@page "/parentWithParam/{parentId}" // URLからパラメータを受け取ることもできる

<!-- 親コンポーネント -->
<Child Title="ここでタイトルを渡す">ここでChildContentを渡す</Child>

@functions {
    [Parameter] private int parentId { get; set; }
}

クラス方式の場合は、[RouteAttribute()] を使用する。

[RouteAttribute("/parent")]
[RouteAttribute("/parent2")]
[RouteAttribute("/parent3/{parentId}")] // これは未確認…
public class ParentComponent : BlazorComponent { ... }

ライフサイクルメソッド

これも React などと似た感じで、コンポーネントのライフサイクルメソッドが用意されている。 実際には違う点も多いが、イメージをつかむための参考までに類似の React メソッドも記載している。

メソッド名 (参考:類似の React メソッド) 用途
OnInit() componentWillMount() コンポーネントの初期化直後に一度だけ呼び出される
OnParametersSet() componentWillReceiveProps() コンポーネント変数が割り当て(変更も含む)されるごとに呼び出される
OnAfterRender() componentDidUpdate() コンポーネントの初期描画が完了、または更新されるごとに呼び出される
ShouldRender() ShouldRender() パフォーマンスチューニングなどで描画を更新したくない場合、ShouldRender()false を返すようにする

使う際には、対象のコンポーネント内で以下のようにメソッドを override すれば良い。

@functions {
    protected override void OnInit() {
        Console.WriteLine("OnInit() called");
    }

    protected override void OnParametersSet() {
        Console.WriteLine("OnParametersSet() called");
    }

    protected override void OnAfterRender() {
        Console.WriteLine("OnAfterRender() called");
    }
}

おまけ:Blazor で Flux するには

たぶん Blazor で Flux しようとする人はいるんだろうなと思ってググったところ、blazor-redux というプロジェクトがすでに存在していた。

github.com

Redux 風の APIC# で実装していて、Redux を知っていればすぐに使えそう。 ちゃんとブラウザ上で Redux DevTools を使って State を見られるようにしてあるようでえらい。

で、こういうのが必要かどうかっていうのは結局 JS のフロント開発と一緒で、開発規模がかなり大きくなれば、必要になってくるんだろうとは思う。

参考

learn-blazor.com

codedaze.io