はじめに
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翻訳先生の力を借りながらチュートリアルを行いました。
もし何か間違いがあったらコメントで教えてください。
セットアップ
- .NET Core SDK(2.1.300、またはそれより新しいバージョン)のインストール。
- **Visual Stduio 2017**のインストール。
- Visual Studio Marketplace から最新の Blazor Language Services 拡張機能をインストール
プロジェクトの作成
Visual Studio を使用する場合
ファイル > 新規作成 > プロジェクト > ASP.NET Core Web アプリケーション
を選択します。
.NET Core
ASP.NET Core 2.1
を指定し Blazor を選択して OK をクリック。
.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
を入力します。
ブラウザの開発者ツールを開くと*.dll
が読み込まれています!
アプリを構築する
Blazor は Visual Studio または 任意のエディタ + .NET Core Cli で開発ができます。
ただ、IDE の支援機能をフルに受けられる Visual Studio のほうがより快適です。
コンポーネントのビルド
アプリの3つのページ、[Home]、[Counter]、[FetchData] の各ページを表示してみます。
これらのページはそれぞれindex.cshtml
Counter.cshtml
FetchData.cshtml
のコンポーネントで構成されています。
Blazorコンポーネントは、ブラウザでコンパイルおよび実行されます。
Counter ページ開きClick me
ボタンを押します。
ボタンが押されるたびに、ページは更新されずにカウンターがインクリメントされます。
このようなクライアント側の動作は通常 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;
}
}
コンポーネントを使用する
定義したコンポーネントは他のコンポーネントのマークアップで HTML タグのように使用できます。
Home
ページにCounter
コンポーネントを追加する
index.chtml
にCounter
コンポーネントを追加します。
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
@*`Counter`コンポーネントを追加する*@
<Counter />
ブラウザを更新しHome
ページにCounter
コンポーネントが追加されているのを確認します。
Counter
ページとHome
ページ上のCounter
は別のインスタンスです。
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 ずつ増えます。
コンポーネントへのルーティング
.cshtml
ファイルの先頭の@page
ディレクティブはコンポーネントがルーティングできるページであることを表します。
Counter
コンポーネントの場合、@page "/counter"
と定義されているので/counter
でアクセスできます。
@page
ディレクティブがない場合、ルートリクエストをハンドルしませんが、先ほどの例のようにほかのコンポーネントで使用することは可能です。
依存オブジェクトの注入(DI)
依存オブジェクトの注入によって、コンポーネントはアプリケーションサービスプロバイダに登録されているサービスを利用できます。
@inject
ディレクティブを使用してコンポーネントにサービスを注入できます。
FetchData.cshtml
のFetchData
コンポーネントの実装を見てみます。
@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>
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
ページに遷移できることを確認します。
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 リストを保持するフィールドを追加します。
@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 ボタンが押されても何も起こりません。
コンポーネントに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 リストが追加できることを確認します。
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>
感想
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 は簡単なアンケートを行っているので、試してみた方はぜひ答えてみてはいかかでしょうか...!?
-
Github のスターが多いリポジトリで機械学習した入力支援人工知能です。ただの補完ではなくコードの文脈に沿った提案をしてくれます。現在 C# のみが対応していますが、ほかの言語も提供予定です。 ↩