WebAssembly でシングルページアプリケーションが開発できる Blazor フレームワークの公式チュートリアルをやったら近未来感が凄かった

![blazor-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/bb6c6f5a-4d37-b609-e9bb-babfd1d04245.png)

## はじめに

**Blazor** は、**WebAssembly** を使用してブラウザで実行される、**.NET**上に構築されたシングルページ Web アプリケーションフレームワークです。

@jsakamoto さんの[C# で Single Page Web Application が書ける Blazor が凄かった件](https://qiita.com/jsakamoto/items/20d4893f6c8cdb0356f6) にとても詳しく載っています。

大まかな仕組みとしては C# をコンパイルした IL（.NET の中間言語）を WebAssembly にコンパイルすることでブラウザで .NET が実行できます。
TypeScript やその他 AltJS のように JavaScript にトランスパイルされるわけではなく、本当に C# のコードがブラウザで動きます。
このような技術は、今は無き Microsoft Silverlight を連想させますが、そうではなく、オープンな Web 標準を使用してブラウザで実行される HTML と CSS に基づく.NET Web フレームワークです。
プラグインは不要で、モバイルデバイスや古いブラウザでも`asm.js`によるフォールバックで動作します。

Blazor はまだ実験段階ですが、次のような最新の Web フレームワークのすべての機能を備えています。

- コンポーネント指向で合成可能な UI
- ルーティング
- レイアウト
- フォームとバリデーション
- 依存オブジェクトの注入
- JavaScript interop
- 開発中のブラウザでのライブリロード
- サーバーサイドレンダリング
- ブラウザと IDE の両方で完全な .NET デバッグ
- 豊富な IntelliSense とツール
- asm.js 経由で古い（ WebAssembly に対応していない）ブラウザでも実行可能
- パブリッシュとアプリサイズのトリミング

今回行ったチュートリアルはこちらです。
**[Get started with Blazor](https://blazor.net/docs/get-started.html)**
Github の公式リポジトリはこちらです。
**[aspnet/Blazor](https://github.com/aspnet/Blazor)**

google翻訳先生の力を借りながらチュートリアルを行いました。
もし何か間違いがあったらコメントで教えてください。


## セットアップ

- **[.NET Core SDK](https://go.microsoft.com/fwlink/?linkid=873092)**（2.1.300、またはそれより新しいバージョン）のインストール。
- **[Visual Stduio 2017](https://go.microsoft.com/fwlink/?linkid=873093)**のインストール。
- Visual Studio Marketplace から最新の **[Blazor Language Services](https://go.microsoft.com/fwlink/?linkid=870389)** 拡張機能をインストール

## プロジェクトの作成

### Visual Studio を使用する場合

`ファイル > 新規作成 > プロジェクト > ASP.NET Core Web アプリケーション`を選択します。
`.NET Core` `ASP.NET Core 2.1`を指定し Blazor を選択して OK をクリック。

![new_project-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/23bf76ac-e1e2-d784-cf3c-03618d9daded.png)
![select_blazor-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/7d16f054-e333-5be5-b004-a36b40a48cf4.png)

### .NET Core Cli を使用する場合

```sh
# Blazor プロジェクトテンプレートのインストール
dotnet new -i Microsoft.AspNetCore.Blazor.Templates
# プロジェクトの作成
dotnet new blazor -o BlazorApp1
# 作成したプロジェクトに移動
cd BlazorApp1
```

### Blazor アプリを実行する

Visual Studio の場合、`Ctrl + F5`を押します。.NET Core Cli の場合`dotnet run`を入力します。

![blazor_run-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/6ee42e41-a776-9e14-3bb5-119f0d40cc08.png)


ブラウザの開発者ツールを開くと`*.dll`が読み込まれています！
![import_dll-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/abfee469-0692-ca70-f6cb-547032c0132c.png)

## アプリを構築する

Blazor は Visual Studio または 任意のエディタ + .NET Core Cli で開発ができます。
ただ、IDE の支援機能をフルに受けられる Visual Studio のほうがより快適です。

### コンポーネントのビルド

アプリの3つのページ、**[Home]**、**[Counter]**、**[FetchData]** の各ページを表示してみます。
これらのページはそれぞれ`index.cshtml` `Counter.cshtml` `FetchData.cshtml`のコンポーネントで構成されています。
Blazorコンポーネントは、ブラウザでコンパイルおよび実行されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/60790f9e-0f80-4d53-a03c-4c76135d4a52.png)

Counter ページ開き`Click me`ボタンを押します。
ボタンが押されるたびに、ページは更新されずにカウンターがインクリメントされます。
![counter_click-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/320b8a6e-7a15-1ccd-7204-52b366a6d634.png)
このようなクライアント側の動作は通常 JavaScript で処理されます。
しかし、Blazor の場合、C# と .NET による`Counter`コンポーネントによって実装されています。

#### `Counter`コンポーネントの実装を見てみる

`Pages/Counter.cshtml`を開きます。
（※Qiitaのシンタックスハイライトに`cshtml`が対応していないので分割していますが**実際は1つのファイル**です。）

```html
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

@*クリックされると IncrementCount が呼ばれる*@
<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>
```

```c#
@functions {
    /* @functions ブロック内にロジックを定義
     * C# ではメンバーのアクセス修飾子を省略すると private になる */

    // フィールドやプロパティでコンポーネントの状態を定義できる
    int currentCount = 0;

    // イベントハンドリングのためのメソッド
    void IncrementCount()
    {
        currentCount++;
    }
}
```

`Counter`コンポーネントの UI は、通常の HTML を使用できます。
動的レンダリングロジック（ループ、条件式、式など）は、**Razor 構文**でマークアップに C# コードを埋め込むことで実現できます。
また、HTML と C# のコンポーネントは、ビルド時に C# のクラスに変換されます。
生成された .NET クラスの名前は、ファイルの名前と一致します。

ボタンが押されると、`Counter`コンポーネントの登録された`onclick`ハンドラで`IncrementCount`が呼び出され、`Counter`コンポーネントはレンダリングツリーを再生成します。
Blazor は新しいレンダリングツリーを前のものと比較し、ブラウザの DOM に変更を適用します。
そして表示されたカウントが更新されます。
DOM の差分更新は React などの仮想 DOM と同じように効率的です。

#### `Counter`コンポーネントを編集してみる

以下のように編集します。

```html
@page "/counter"

@*ヘッダーをエキサイティングに編集*@
<h1><em>Counter!!</em></h1> 
```

```cs
@functions {
    int currentCount = 0;

    void IncrementCount()
    {
        // カウントの増分を 2 に変更
        currentCount += 2;
    }
}
```

ブラウザをリロードし変更を確認します。
![double_increment-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/f6915fee-af92-5be9-762d-ed80ee2b7055.png)

### コンポーネントを使用する

定義したコンポーネントは他のコンポーネントのマークアップで HTML タグのように使用できます。

#### `Home`ページに`Counter`コンポーネントを追加する

`index.chtml`に`Counter`コンポーネントを追加します。

```html
@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

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

@*`Counter`コンポーネントを追加する*@
<Counter />
```

ブラウザを更新し`Home`ページに`Counter`コンポーネントが追加されているのを確認します。
![using_component-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/e11fd7d9-9a91-2917-3f0f-effe56245dd3.png)
`Counter`ページと`Home`ページ上の`Counter`は別のインスタンスです。
![separated_instance-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/d38df72d-ae34-5c5b-9d6e-2ea96a0b2e2c.png)

`Counter`コンポーネントの`currentCount`をインスタンス変数からクラス変数に変更する(`int currentCount = 0;` => `static int currentCount = 0;`)と値が共有します。

### コンポーネントのパラメーター

コンポーネントは外部から渡されるパラメーターを持つことができます。
カウンターの増分をパラメータとして定義してみます。

```cs
@functions {
    int currentCount = 0;

    [Parameter] // Parameter 属性で修飾する
    int IncrementAmount { get; set; } = 1; // デフォルトの増分を 1 に

    void IncrementCount()
    {
        // 増分をプロパティ IncrementAmount に変更
        currentCount += IncrementAmount;
    }
}
/* Visual Studioでは、paraスニペットを使用してコンポーネントパラメータをすばやく追加できます。
 * para を入力し、Tabキーを2回押します。*/
```

`Home`ページ(`index.cshtml`)に追加した`Counter`コンポーネントに`IncrementCount`属性を設定します。
`Counter`コンポーネントの`IncrementCount`はアクセスレベルは`private`ですが、`[parameter]`属性がついているため外部から設定できます。
React の `Props`っぽいですね。

```html
@*増分を 10 に設定*@
<Counter IncrementAmount="10" />
```

ブラウザをリロードし変更を確認します。
`Home`ページ上のカウンターは 10 ずつ増えますが、`Counter`ページでは 1 ずつ増えます。
![params_counter-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/dede277e-7077-5dd9-3dc2-bccd8ee9a14f.png)

### コンポーネントへのルーティング

`.cshtml`ファイルの先頭の`@page`ディレクティブはコンポーネントがルーティングできるページであることを表します。
`Counter`コンポーネントの場合、`@page "/counter"`と定義されているので`/counter`でアクセスできます。
`@page`ディレクティブがない場合、ルートリクエストをハンドルしませんが、先ほどの例のようにほかのコンポーネントで使用することは可能です。

### 依存オブジェクトの注入（DI）

依存オブジェクトの注入によって、コンポーネントはアプリケーションサービスプロバイダに登録されているサービスを利用できます。
`@inject`ディレクティブを使用してコンポーネントにサービスを注入できます。

`FetchData.cshtml`の`FetchData`コンポーネントの実装を見てみます。
`@inject`ディレクティブは、`HttpClient`のインスタンスをコンポーネントに注入するために使用されます。

```html
@page "/fetchdata"
@inject HttpClient Http 
@* ↑ HttpClient のインスタンスを注入する*@

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>
```

`FetchData`コンポーネントは、注入された`HttpClient`使用して、コンポーネントが初期化されたときに、サーバーから JSON データを取得します。

```cs
@functions {
    WeatherForecast[] forecasts;

    // コンポーネントが初期化された際、非同期で天気予報データ取得
    protected override async Task OnInitAsync()
    {
        // 注入されたブラウザの Fetch API を呼び出してリクエストを送信する
        // 取得された Json は WeatherForecast の配列にデシリアライズされる
        forecasts = await Http.GetJsonAsync<WeatherForecast[]>("sample-data/weather.json");
    }

    //天気予報オブジェクトの型
    class WeatherForecast
    {
        public DateTime Date { get; set; }
        public int TemperatureC { get; set; }
        public int TemperatureF { get; set; }
        public string Summary { get; set; }
    }
}
```

`@foreach` ループで、取得した天気予報オブジェクトのインスタンスをレンダリングします。

```html
<table class="table">
    <thead>
        <tr>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </tr>
    </thead>
    <tbody>
        @*foreach ループで取得した天気予報データの配列を表示*@
        @foreach (var forecast in forecasts)
        {
            <tr>
                <td>@forecast.Date.ToShortDateString()</td>
                <td>@forecast.TemperatureC</td>
                <td>@forecast.TemperatureF</td>
                <td>@forecast.Summary</td>
            </tr>
        }
    </tbody>
</table>
```

![fetchdata-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/14c39d31-54ca-7e4e-7362-88d72b06ae96.png)


### ToDo リストを作成する

単純な ToDo リストを実装する新しいページをアプリケーションに追加します。

#### `Todo`ページの追加

空のテキストファイルを`Pages`フォルダに`Todo.cshtml`という名前で保存します。
最初にページにマークアップを追加します。

```html
@page "/todo"

<h1>Todo</h1>
```

`Shared/NavMenu.cshtml`を編集して、`Todo`ページをナビゲーションバーに追加します。

```html
<div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu>
        @* 略 *@
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="todo">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Todo
            </NavLink>
        </li>
    </ul>
</div>
```

ブラウザを更新して`Todo`ページに遷移できることを確認します。

![add_todo_component-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/4584d585-4971-fc3b-3edc-32237e584ae0.png)

#### `TodoItem`クラスの追加

プロジェクトのルートに ToDo 項目を表す`TodoItem.cs`を追加します。
以下のように編集します。

```cs
namespace BlazorApp1
{
    public class TodoItem
    {
        // タイトル
        public string Title { get; set; }
        // 終わらせたかどうか
        public bool IsDone { get; set; }
    }
}
```

#### `Todo`コンポーネントの編集

`Todo`コンポーネント(`Todo.cshtml`)に戻り、`@functions`ブロック内に Todo リストを保持するフィールドを追加します。

```cs:Todo.cshtml
@functions {
    // Todo 項目をリストとして持つ
    IList<TodoItem> todos = new List<TodoItem>();
}
```

foreachループを追加して、各 Todo 項目をリストとしてレンダリングします。

```html
@page "/todo"

<h1>Todo</h1>

<ul>
    @* foreach で各 Todo 項目のリストをレンダリング *@
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>
```

アプリには、Todo リストを追加するための UI 要素が必要です。
リストの下にテキストボックスとボタンを追加します。

```html
@page "/todo"

<h1>Todo</h1>

<ul>
    @foreach (var todo in todos)
    {
        <li>@todo.Title</li>
    }
</ul>

@* テキストボックスと Todo 追加ボタン *@
<input placeholder="Something todo" />
<button>Add todo</button>
````

ブラウザを更新して変更を確認します。
ボタンにイベントハンドラが接続されていないため、Add todo ボタンが押されても何も起こりません。
![todo_ui-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/cac4e78d-f4a4-9554-32e8-7c63b37c2bd1.png)

コンポーネントに`AddTodo`メソッドを追加し、ボタンの`onclick`属性に登録します。

```html
<input placeholder="Something todo" />
@* onclick="@AddTodo"を追加 *@
<button onclick="@AddTodo">Add todo</button>
```

```cs
@functions {
    IList<TodoItem> todos = new List<TodoItem>();

    // Todo 追加メソッド。ボタンが押されるたびに呼ばれる。
    void AddTodo()
    {
        // Todo: Add the todo
    }
}
```

新しい ToDo 項目のタイトルを取得するには、`newTodo`フィールドを追加し、`bind`属性を使用してテキスト入力の値にバインドします。

```cs
    IList<TodoItem> todos = new List<TodoItem>();
    string newTodo; // private フィールドを追加する
```

```html
@* bind 属性でテキストボックスのテキストと フィールド newTodo をバインドする *@
<input placeholder="Something todo" bind="@newTodo" />
```

`AddTodo`メソッドを編集して Todo リストに Todo を追加できるようにします。

```cs
void AddTodo()
{
    if (string.IsNullOrWhiteSpace(newTodo))
    {
        // テキストボックスが空なら追加しない
        return;
    }
    // Todo リストに新しい Todo を追加する
    todos.Add(new TodoItem { Title = newTodo });
    // テキストボックスを空に戻す。
    newTodo = "";
}
```

ブラウザを更新し Todo リストが追加できることを確認します。
![add_todo-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/8bcd148d-1656-d4b7-5628-5b97a01aa4cf.png)


Todo リストにチェックボックスを追加し終わらせたかどうかをチェックできるようにします。
また、リストの各 Todo の内容を変更できるようにします。

```html
<ul>
    @foreach (var todo in todos)
    {
        <li>
            @* チェックボックスのチェック状態を todo.IsDone にバインド *@
            <input type="checkbox" bind="@todo.IsDone" />
            @* todo.Title をテキストボックスにバインドし編集できるように *@
            <input bind="@todo.Title" />
        </li>
    }
</ul>
```

終わらせていない Todo の数を表示します。

```html
<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>
```

ブラウザを更新して変更を確認します。
![todolist.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/86110f32-93a6-1f2c-a8a0-5fd0a5343773.png)

## 感想

WebAssembly で SPA を開発するの面白そうだな、でもちょっと難しいかも...と思っていましたが、拍子抜けするほど簡単でした。
まだまだ足りないところも多いですが、後発なだけあって React、Vue、Angular などのいいとこどりしようという気概を感じます。
Visual Studio の支援はすごいので C#er にとっては JS のフレームワークを使うより快適に SPA の開発ができそうです。
文字列すら入力補完や型チェックが働いて HTML テンプレートがゴリゴリ書けます。

Visual Studio の拡張機能には AI がコーディング支援してくれる [Visual Studio IntelliCode](https://visualstudio.microsoft.com/ja/services/intellicode/)[^1] 、開発者生産性向上ツール Jet Brains ReSharper などがありますが、これらと Visual Studio の機能を合わせると、もはや、少しの入力と`Ctrl`+`space`による補完や`Ctrl`+`.`によるリファクタリング機能だけで SPA 作れちゃいそうな感じさえします(笑)

また、現在 Blazor は簡単なアンケートを行っているので、試してみた方はぜひ答えてみてはいかかでしょうか...！？

![questionnaire-min.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/09a48e6a-0d92-d33d-4f68-cd2f17945f52.png)

[^1]: Github のスターが多いリポジトリで機械学習した入力支援人工知能です。ただの補完ではなくコードの文脈に沿った提案をしてくれます。現在 C# のみが対応していますが、ほかの言語も提供予定です。