C#
SPA
WebAssembly
ASP.NET_Core
Blazor

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

blazor-min.PNG

はじめに

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

@jsakamoto さんのC# で Single Page Web Application が書ける Blazor が凄かった件 にとても詳しく載っています。

大まかな仕組みとしては 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
Github の公式リポジトリはこちらです。
aspnet/Blazor

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

セットアップ

プロジェクトの作成

Visual Studio を使用する場合

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

new_project-min.PNG
select_blazor-min.PNG

.NET Core Cli を使用する場合

# 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

ブラウザの開発者ツールを開くと*.dllが読み込まれています!
import_dll-min.PNG

アプリを構築する

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

コンポーネントのビルド

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

image.png

Counter ページ開きClick meボタンを押します。
ボタンが押されるたびに、ページは更新されずにカウンターがインクリメントされます。
counter_click-min.PNG
このようなクライアント側の動作は通常 JavaScript で処理されます。
しかし、Blazor の場合、C# と .NET によるCounterコンポーネントによって実装されています。

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

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

@page "/counter"

<h1>Counter</h1>

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

@*クリックされると IncrementCount が呼ばれる*@
<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>
@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コンポーネントを編集してみる

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

@page "/counter"

@*ヘッダーをエキサイティングに編集*@
<h1><em>Counter!!</em></h1> 
@functions {
    int currentCount = 0;

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

ブラウザをリロードし変更を確認します。
double_increment-min.PNG

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

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

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

index.chtmlCounterコンポーネントを追加します。

@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
CounterページとHomeページ上のCounterは別のインスタンスです。
separated_instance-min.PNG

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

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

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

@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っぽいですね。

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

ブラウザをリロードし変更を確認します。
Homeページ上のカウンターは 10 ずつ増えますが、Counterページでは 1 ずつ増えます。
params_counter-min.PNG

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

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

依存オブジェクトの注入(DI)

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

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

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

<h1>Weather forecast</h1>

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

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

@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 ループで、取得した天気予報オブジェクトのインスタンスをレンダリングします。

<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

ToDo リストを作成する

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

Todoページの追加

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

@page "/todo"

<h1>Todo</h1>

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

<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

TodoItemクラスの追加

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

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

Todoコンポーネントの編集

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

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

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

@page "/todo"

<h1>Todo</h1>

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

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

@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

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

<input placeholder="Something todo" />
@* onclick="@AddTodo"を追加 *@
<button onclick="@AddTodo">Add todo</button>
@functions {
    IList<TodoItem> todos = new List<TodoItem>();

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

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

    IList<TodoItem> todos = new List<TodoItem>();
    string newTodo; // private フィールドを追加する
@* bind 属性でテキストボックスのテキストと フィールド newTodo をバインドする *@
<input placeholder="Something todo" bind="@newTodo" />

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

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

ブラウザを更新し Todo リストが追加できることを確認します。
add_todo-min.PNG

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

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

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

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

ブラウザを更新して変更を確認します。
todolist.PNG

感想

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

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

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

questionnaire-min.PNG


SI 企業所属の2年目プログラマーです。
エンジニアの方とつながれると嬉しいです!:grinning:
twitter: のさ@nosa_programmer


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