BlazorでSPAするぞ!(1)

※ 2019/6/15 .NET Core 3.0 Preview 6 に対応した記述に修正しました。
以前からBlazorに興味を持ちつつもExperimentalだしなぁ、SPAは まぁ React+Redux で安定的な感じでいいんじゃね? などと思っていたら 2019/4/18 にExperimentalから正式Previewになったので、ちょろちょろ勉強してみました。

https://devblogs.microsoft.com/aspnet/blazor-now-in-official-preview/

個人的には当面、仕事で使うわけでもなく、忘れてしまいそうなので得た知識をBlogという形で書き記しておきます。
間違ってるか所あったら指摘よろ。

1. Blazorとは

公式Docsでは以下のように書かれています。

Blazor is a framework for building interactive client-side web UI with .NET

「Blazorは、.NETでインタラクティブなクライアントサイドWebユーザーインターフェイスを構築するためのフレームワークです。」ってことですね。
エンジニア的には、抽象的なモヤモヤ感のある文章ですね。
具体的に出来ることを簡単に言うと、今までJavaScript(+各種JSフレームワーク)でやってた事を、.NET Core(つまりC#)で作ることができるよ、ってものです。
JavaScript(or TypeScript) + React」とか「JavaScript(or TypeScript) + Vue」とかで作ってたようなSPAなアプリケーションをBlazorで作ることができます。
UI要素はHTML・CSSで構築し、ロジック部分をC#で記述するようなイメージになります。
メリットとしては「サーバーサイドもクライアントサイドもC#で開発できる」ってことです。
そもそもサーバーサイドにおいて非.NETなエンジニアにとっては、メリットとはならないものですが、.NETディベロッパーにとってはクライアントサイドもC#で書けるというのは生産性とかメンテナンス性とかでメリットがあります。
従来のWeb開発の React/Redux/TypeScript/Flow/Vue/Angular/Babel... の技術たちと比べるとキワ物感がありますが(Blazorは.NETに絞られる技術になるので)、ASP.NET Core+BlazorでのSPA開発は、かなり生産性高そうという印象を持っています(まだPreviewだし、実開発してない段階での感想ですが)。

1.1. WebAssembly

C#コードがクライアントサイドで動作するというキーワードは、過去にもいくつかありましたね。。。

2019年5月現在においては、オープンな技術開発的には使えない技術達ですね(一部のtoBでは生き残ってると思いますが・・・)。

で、Blazorがクライアントサイド(ブラウザ)でC#コードの動作を実現している仕組みですが、「WebAssembly」になります。
WebAssemblyは C/C++・Rust・Go言語 なんかで作れるようになっていますが、BlazorによってC#でもWebAssemblyが作れるようになりました。
厳密には、Blazorの.NET CoreアセンブリをMono WASMでWebAssemblyにビルドして実行するような形になります。

図にすると以下のような感じ。

f:id:daigo-knowlbo:20190501214517p:plain

BlazorとDOMとの間に矢印がありますが、Blazorの中でプログラムによって画面要素に変更があるとそれがDOMに伝えられて画面の更新が行われます。
Reactの仮想DOMと同様に、Blazorでは「render tree」と呼ばれるUI要素構造をメモリに展開し、変更の必要がある部分のみDOMに反映させる仕組みを持っています。

1.2. Client-Side BlazorとServer-Side Blazor

Blazorは上述のようにクライアントサイドでC#が動作する技術(Client-Side Blazor)ですが、これ以外に同じ実装をサーバーサイドで動作させるServer-Side Blazorというものがあります。
Blazorコンポーネントをサーバーサイドで実行する仕組みのものになります。クライアントサイドはプレーンなHTML+JSでWebAssemblyは実行されません。
実行イメージは以下のような感じ。

f:id:daigo-knowlbo:20190501214537p:plain

ボタンのクリックなど「Blazor的に影響のある事象が発生する」と、Signal Rでサーバにリクエストが行われ、サーバサイド(ASP.NET Core)でBlazorが動作し、結果をクライアントに送り返す。結果に応じたDOMの更新がクライアントで行われます(Web Formのポストバックみたいな処理が裏側で自動的に行われます)。

※ この投稿では特別に記述がない限り Client-Side Blazor を前提とした説明を行います。

2. 開発環境構築

開発環境はWindowsでもMacでも行けますが、ここではWindowsを前提とします。

2.1. .net Core 3インストール

以下から.NET Core 3.0 Preview SDKをダウンロードしてインストールします。

https://dotnet.microsoft.com/download/dotnet-core/3.0

コマンドプロンプトでバージョンを確認。

C:>dotnet --version 3.0.100-preview6-012264

※2019/5/1現在の最新は 3.0.100-preview4-011223
※2019/6/15現在の最新は 3.0.100-preview6-012264

2.2. Blazorテンプレートのインストール

コマンドプロンプトで以下のコマンドを実行します。
(dotnet newコマンドに、Blazorのプロジェクトテンプレートがインストールされます)

dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview6.19307.2

2.3. C# for Visual Studio Codeのインストール(Visual Studio Code利用の場合)

Visual Studio Codeを使う場合は、「C# for Visual Studio Code」をインストールしておきます。

https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp

2.4. Visual Studio Extensionインストール(Visual Studio 2019利用の場合)

Visual Studio 2019を使う場合は、「BlazorのExtension」をインストールしておきます。
(Visual Studio 2019に、Blazorのプロジェクトテンプレートがインストールされます)

https://marketplace.visualstudio.com/items?itemName=aspnet.blazor

※本記事では Visual Studio Code を開発に利用します。(VS2019だと.razorのファイルの追加がUIから出来ない等Extensionが十分にPreview版に追い付いていない感があったので)

3. Blazorプロジェクト作成

では早速Blazorプロジェクトを作成します。

3.1. dotnet コマンド / 4つのプロジェクトテンプレート

VS Codeベースで進めるのでdotnetコマンドを使っていきます。

まずプロジェクトテンプレートの確認を。
dotnet new」でテンプレート一覧を確認すると以下のようにBlazor関連のテンプレートが4種類表示されます。

C:\>dotnet new
使用法: new [オプション]
...省略

Templates                                         Short Name            Language          Tags
-------------------------------------------------------------------------------------------------------------------------------
...省略
Blazor (server-side)                              blazorserverside      [C#]              Web/Blazor
Blazor (ASP.NET Core hosted)                      blazorhosted          [C#]              Web/Blazor/Hosted
Blazor Library                                    blazorlib             [C#]              Web/Blazor/Library
Blazor (client-side)                              blazor                [C#]              Web/Blazor/Standalone
...省略

それぞれ以下の通りです。

(1) Blazor (server-side)

Server side BlazorのASP.NET Coreプロジェクトテンプレートです。

dotnet new blazorserverside -o blazor_serverside

作成されるファイル達↓↓↓

f:id:daigo-knowlbo:20190501214557p:plain

(2) Blazor (ASP.NET Core hosted)

Client side Blazor + BlazorにWebAPIを提供するASP.NET Core + Sharedライブラリ(Client side BlazorとASP.NET Coreサーバとの間でモデルクラス等を共通するライブラリ)のテンプレートです。

dotnet new blazorhosted -o blazor_hosted

作成されるファイル達↓↓↓
f:id:daigo-knowlbo:20190501214618p:plain

(3) Blazor Library

Blazorコンポーネントのライブラリプロジェクトのテンプレートです。

dotnet new blazorlib -o blazor_lib

作成されるファイル達↓↓↓
f:id:daigo-knowlbo:20190501214636p:plain

(4) Blazor (client-side)

Client side Blazorのプロジェクトテンプレートです。

dotnet new blazor -o blazor

作成されるファイル達↓↓↓
f:id:daigo-knowlbo:20190501214648p:plain

4. Hello Blazor

では、純粋にBlazorを試すために「Blazor (client-side)」でプロジェクトを作成します。

ますプロジェクトを作成。hello_blazorという名前にします。

dotnet new blazor -o hello_blazor

4.1. Visual Studio Codeで開く

VS Codeの「File - Open Folder」メニューで 作成した hello_blazor フォルダを開きます。

f:id:daigo-knowlbo:20190501214702p:plain

4.2. ビルド + 実行する

ある程度の動作する実装がテンプレートで出力されているので、ひとまずそのままビルドします。
コマンドプロンプトでプロジェクトフォルダに移動し、「dotnet build」コマンドを実行します。

C:\workspace\for_blog\hello_blazor>dotnet build
.NET Core 向け Microsoft (R) Build Engine バージョン 16.2.0-preview-19278-01+d635043bd
Copyright (C) Microsoft Corporation.All rights reserved.

  C:\cbt\github\hello_blazor\hello_blazor.csproj の復元が 21.57 ms で完了しました。
C:\Program Files\dotnet\sdk\3.0.100-preview6-012264\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(158,5): message NETSDK1057: プレビュー版の .NET Core を使用しています。https://aka.ms/dotnet-core-preview をご覧ください [C:\cbt\github\hello_blazor\hello_blazor.csproj]
  hello_blazor -> C:\cbt\github\hello_blazor\bin\Debug\netstandard2.0\hello_blazor.dll
  Processing embedded resource linker descriptor: mscorlib.xml
  Duplicate preserve in resource mscorlib.xml in mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e of System.Threading.WasmRuntime (All).  Duplicate uses (All)
  Type System.Reflection.Assembly has no fields to preserve
  Type Mono.ValueTuple has no fields to preserve
  Output action:     Link assembly: hello_blazor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
  Output action:     Save assembly: System.Threading.Tasks.Extensions, Version=4.2.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:     Save assembly: System.Text.Json, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:     Save assembly: System.Runtime.CompilerServices.Unsafe, Version=4.0.5.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Output action:     Save assembly: System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Output action:     Save assembly: System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:     Save assembly: System.ComponentModel.Annotations, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Output action:     Save assembly: System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:     Save assembly: Mono.WebAssembly.Interop, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.JSInterop, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.Extensions.Primitives, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.Extensions.Options, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.Extensions.Logging.Abstractions, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.Extensions.DependencyInjection.Abstractions, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.Extensions.DependencyInjection, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.AspNetCore.Metadata, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.AspNetCore.Components.Browser, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.AspNetCore.Components, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.AspNetCore.Blazor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.AspNetCore.Authorization, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:   Delete assembly: netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:     Link assembly: mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
  Output action:     Link assembly: System, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
  Output action:     Link assembly: Mono.Security, Version=2.0.5.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756
  Output action:   Delete assembly: System.Xml, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
  Output action:   Delete assembly: System.Numerics, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:     Link assembly: System.Core, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
  Output action:   Delete assembly: System.Data, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:   Delete assembly: System.Drawing.Common, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:   Delete assembly: System.IO.Compression, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:   Delete assembly: System.IO.Compression.FileSystem, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:   Delete assembly: System.ComponentModel.Composition, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:     Link assembly: System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Output action:   Delete assembly: System.Runtime.Serialization, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
  Output action:   Delete assembly: System.Transactions, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:   Delete assembly: System.Web.Services, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Output action:   Delete assembly: System.Xml.Linq, Version=2.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
  Output action:   Delete assembly: System.ServiceModel.Internals, Version=0.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
  Output action:   Delete assembly: System.Runtime, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Writing boot data to: C:\cbt\github\hello_blazor\obj\Debug\netstandard2.0\blazor\blazor.boot.json
  Blazor Build result -> 31 files in C:\cbt\github\hello_blazor\bin\Debug\netstandard2.0\dist

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:09.00

obj / binフォルダが作成され、その下にアセンブリ(.dll)が出力されました。

では以下のように dotnet run コマンドを実行します。

C:\workspace\for_blog\hello_blazor>dotnet run
Hosting environment: Production
Content root path: C:\workspace\for_blog\hello_blazor
Now listening on: http://localhost:5000
Now listening on: https://localhost:5001
Application started. Press Ctrl+C to shut down.

Blazor自体はClient sideのWebAssembly(の上で動く.NETアセンブリ)ですが、http://localhost:5000 / https://localhost:5001 としてhello_blazorをホストするHttpサーバが立ち上がります。
ブラウザで http://localhost:5000 にアクセスすると、テンプレートで出力したサンプル実装が起動します。

f:id:daigo-knowlbo:20190501214729p:plain

Hello World以上の立派な実装がなされています。

ちなみにこのページを表示した時のHttpRequestの状況ですが、以下のようになっています。

f:id:daigo-knowlbo:20190501214743p:plain

よく見ると「mono.js」とか「mono.wasm」とか「hello_blazor.dll」とか「Microsoft.AspNetCore.Blazor.dll」とか・・・.NETが動こうとしている事が見て取れると思います。

それから、左のサイドメニューから「+Counter」を選択すると、ボタンクリックでカウントアップが行われる画面に移動します。
「Click me」ボタンをクリックすると、Current Countが1ずつカウントアップしますが、この時サーバへのリクエストが飛んでいないことも確認できます。
(当然ながらクライアントサイドでC#コードが実行されているので、このようなロジック処理でサーバへのラウンドトリップは発生しない、という事です)

f:id:daigo-knowlbo:20190501214802p:plain

4.3. Hello Blazorに着手!

何もしないで http://localhost:5000 にアクセスした画面がすでに「Hello, World!」になっています。
これを「Hello Blazor」に改造していこうと思います。
Blazorが動作するベースの仕組みは色々ありますが、、、とりあえず このページは「Pages/index.razor」で実装されています。

# Pages/Index.razor #

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

実行画面と見比べると 大体想像できる感じ ですよね(HTMLタグが書かれていて、ASP.NET CoreのRazorを知っていれば @xx なRazor構文が書かれているな・・と)。
画面上にヘッダーやサイドメニューが付いてるのはレイアウト設定が効いているからです(ASP.NET Coreの _Layout.cshtml とかと同じような仕組みがBlazorにもあり、それが適用されています )。
先頭の「@page "/"」は後(後続の投稿)で詳細を説明しますが、ルーティング設定です。"/"なのでルートとしてこのIndex.razorがルーティング適用されています。

(1) プロパティ変数の利用

以下のような画面に改造したいと思います。

f:id:daigo-knowlbo:20190501214817p:plain

HTMLをべた書きすれば実現できるのですが、それではあまり意味がないので、無理やりC#なコードを使っていきます。

# Pages/Index.razor(Hello Blazor版 その1) #

@page "/"

<h1>@Message</h1>

@code {
  private string Message = "Hello Blazor";
}

「@Message」は、C#で定義した変数(プロパティ)をレンダリングします。ASP.NETのRazorと完全に一緒です。
「@code」は、このコンポーネント(Index.razor)のコードブロックを表します。コードブロックには、C#で.NETコードを記述することができます。
string型のMessage変数を定義し、"Hello Blazor"で初期化しているのは見れば分かるでしょう。

こんな感じでクライアントサイド実装をC#で行うことができます。

(2) イベントの使用

では、次に以下のような画面に改造します。
「get date and time」ボタンを用意し、クリックされたらすぐ上にテキストで現在日時を表示します。

f:id:daigo-knowlbo:20190501214833p:plain

# Pages/index.razor(Hello Blazor版 その2) #

@page "/"

<h1>@Message</h1>
<br />
CurrentDateTime:@CurrentDateTime
<br />
<input type="button" @onclick="@GetDateTime" value="get date and time" />

@code {
  private string Message = "Hello Blazor";
  private string CurrentDateTime = "???";
  
  private void GetDateTime() {
    CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
  }
}
@CurrentDateTime

CurrentDateTime変数を用意し(初期値は ??? )、「CurrentDateTime:@CurrentDateTime」として画面に表示しています。

ボタン

「<input type="button" @onclick="@GetDateTime" value="get date and timee" />」でボタンを配置しています。
「@onclick="@GetDateTime"」がポイントで、onclickはHTML/JavaScriptにおけるinputタグのイベントと同様です。
これに対して GetDateTime() メソッドをバインディングしています(onclickに対してJavaScriptのFunctionをバインドするのと同じ感覚ですね)。
補足としては、GetDateTime()は @code{} コードブロックで定義したメソッドであり、C#メソッドの呼び出しなのでRazor構文の「@」付けて呼び出しを記述します。
GetDateTime()では DateTime.Now.ToString() という.NETのC#コードにより CurrentDateTime 変数を変更しています。
CurrentDateTimeの変更を受けBlazor Runtimeは、対象箇所の再レンダリングを行います。

良い感じにプロパティとイベントをC#で実装し、HTML要素に反映することが出来ましたね。

※ 「@onclick="@GetDateTime"」の@GetDateTime部分は将来的に「@プレフィックス」を不要とする方針のようです。

4.4. CodeBehindを使う

上述のIndex.razorは、.razorファイルに UI要素 も コード も混ざった状態になってしまいました。
よくある話ですが、Blazorでも UIとコード は分離可能です。

# Pages/Index.razor(code behind版) #

@page "/"
@inherits hello_blazor.Pages.IndexBase

<h1>@Message</h1>
<br />
CurrentDateTime:@CurrentDateTime
<br />
<input type="button" @onclick="@GetDateTime" value="get date and time" />
# Pages/IndexBase.cs(code behind版) #

using System;
using Microsoft.AspNetCore.Components;

namespace hello_blazor.Pages
{
  public class IndexBase : ComponentBase
  {
    public string Message { get; set; } = "Hello Blazor";

    public string CurrentDateTime { get; set; }= "???";

    public void GetDateTime() {
      CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }
  }
}

(1) @inherits

.razorファイルに「@inherits 【クラス名】」とすることでCodeBehindファイルを指定することができます。
で、CodeBehindクラス側は、ここでは「Pages/IndexBase.cs」としましたが、どこに置いてもどんな名前でも問題はありません。
大事なのは、コードビハインドクラスは「Microsoft.AspNetCore.Components.ComponentBase」を継承するという事です。

(2) プロパティとメソッド

Message / CurrentDateTimeが、フィールドとして定義していて不格好だったのでgetter / setterを持つプロパティとしました。
また、2つのプロパティおよびGetDateTime()メソッドはアクセス修飾子を private → public に変更しています。
private だとビルドエラーになります。最低限必要なのは protected です。
.razor上の @code{}内では private で動作したものが、CodeBehind版ではprotected以上のアクセス権限が必要になった理由は、ビルド後の hello_blazor.dll をIlSpy等で確認すると意味が理解できます。

f:id:daigo-knowlbo:20190501214853p:plain

Index.razorはコンパイルされ「Indexクラス」となりました。
Indexクラスは「IndexBaseクラス」を継承しています。IndexBaseクラスは「Pages/IndexBase.cs」で定義したCodeBehindクラスです。
つまり、Index <- IndexBase という継承関係ですからIndexBaseに定義するプロパティ・メソッドはprotectedもしくはpublicでなければIndexからは参照できなくなっています。

アーキテクチャ的には MVVM で実装するとかの選択肢もあるので、コードを code{} に書くとか コードビハインドに書くとか がベストプラクティスどうかは、プロジェクトごとの選択になります。

5. まとめ

という感じで、Blazorではじめの一歩が踏み出せました!

次は ryuichi111std.hatenablog.com