JavaScript なしには何もし得ない Blazor は無駄なのか
ここ数日、下記記事を連投しておりました。
いずれも Blazor から JavaScript を利用するにあたっての各種技法についての話題でした。これら連投した記事を見ていると、「C# でクライアント Web アプリケーションが書ける!」のが謳い文句である Blazor であっても、結局どこかで JavaScript を書く羽目になるのではないか、と感じている方も少なくないかもしれませんね。
そして Blazor そのものも、つまるところは JavaScript による各種支援実装の上に構築されているわけで、その意味では 「Blazor にだけできて、JavaScript にはできないこと、は無い」 と言えます。実際、Qiita ではなくてよその投稿システムでの話ですが、私が喜々として投稿した Blazor プログラミングについての記事に対し、
「Blazor は、JavaScript でできること以上のことができるわけでもない、無駄なプロダクト」
とコメントされたこともあります (実話です)。
まぁ、確かに、とくに Blazor WebAssembly は、MSIL のインタープリタを WebAssembly 上で実行しているのですから (AOT なしのビルドの場合)、JavaScript ネイティブのコードに比べたら処理速度の面でも圧倒的に不利で、わざわざそんな .NET 実行環境レイヤーを積んでまで何やってるんだ、という感想もわからなくはないです。
しかしです。
本当にそのような実行環境のレイヤーを積むのは無駄なのでしょうか。
結局は Web ブラウザの fetch
を呼び出しているだけなのですから、axios
を使うのは無駄なのでしょうか。
Vanilla JS で書けるんだから、わざわざバンドラーによるビルド時間を割いてまで Angular や React や Vue などのライブラリを使うのは無駄なのでしょうか。
いやいや、そんなことはないですよね。
原始的な要素が集まり、組織されたときには、その上位レイヤーとして新たな法則や世界が開けるのです。「コンピュータはつまるところトランジスタの集合なんだから、トランジスタの On/Off が理解できれば、そのコンピュータ上で動く "航空便チケット予約システム" も理解できるかといったら、そんなことないだろ」 とかいう話をどこかで見聞きした覚えもあります。レイヤーを積むっていうのはそういう事なんだと自分は思ってます。
日時を含む JSON データを REST API サーバーから取得するとき
Blazor に話を戻しましょう。
自分が Blazor でクライアント Web アプリケーションを実装している際に「あー、ここは Blazor のお陰で楽させてもらったなー」と思うときがあります。それは、JSON 形式で応答を返す REST API サーバーから、日時型をメンバーに含むデータを HTTP 要求で取得するとき 、です。
具体的に例を出しましょう。
下記のような "天気予報データ" JSON を返す REST API サーバーがあり、このサーバーから天気予報データを取得してページ上に表示する、そのような Blazor アプリケーションを作ろうとしているとします。
[
// 日時が含まれている 👇
{"date":"2022-12-25T10:01:58.610229+09:00", "temperatureC":4, "summary":"Cool"},
{"date":"2022-12-26T10:01:58.610229+09:00", "temperatureC":3, "summary":"Warm"},
{"date":"2022-12-27T10:01:58.610229+09:00", "temperatureC":4, "summary":"Freezing"},
{"date":"2022-12-28T10:01:58.610229+09:00", "temperatureC":2, "summary":"Freezing"},
...
]
Blazor 側では、上記 JSON データを受け取るように、以下のように C# クラスを実装します。
public class WeatherForecast
{
public DateTime Date { get; set; } // 👈 ここで DateTime 型を使う
public int TemperatureC { get; set; }
public string? Summary { get; set; }
}
あとは、Blazor 側の Razor コンポーネントで、REST API から天気予報データを WeatherForecast
型の集合として fetch して (下記)、
@inject HttpClient Http
...
@code {
private IEnumerable<WeatherForecast>? _Forecasts = null;
protected override async Task OnInitializedAsync() {
_Forecasts = await Http.GetFromJsonAsync<IEnumerable<WeatherForecast>>(
"https://.../api/weatherforecast");
}
}
そうして fetch して得られた天気予報データ WeatherForecast
の集合を DOM にレンダリングします。
...
@if (_Forecasts != null) {
<table>
@foreach (forecast in _Forecasts) {
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.Summary</td>
</tr>
}
</table>
}
@code {
...
「なんだ、それがどうした」というところでありますが、注目ポイントは、REST API サーバーから JSON 形式で送られてきた日時型の項目の表示です。Blazor の場合、とくに深いこと考えずに素で、下記のとおり、DateTime
型の ToShortDateString
メソッドを呼び出して、時刻を除いた日付のみをレンダリングできます よね。
...
<td>@forecast.Date.ToShortDateString()</td>
...
これ、一般的な JavaScript によるクライアント Web アプリケーション実装だとどうなりますか?
例えばコーディングは生の JavaScript じゃなくて TypeScript で記述しているとして、天気予報データの各項目の型を以下のとおり記述し、
export type WeatherForecast = {
date: Date, // 👈 ここで Date 型を使う
temperatureC: number,
summary: string
}
例えば React で以下の様に実装したとします。
...
export const App = () => {
const [forecasts, setForecasts] = useState<WeatherForecast[]>([]);
useEffect(() => {
fetch("https://.../api/weatherforecast")
.then(res => res.json())
.then(forecasts => setForecasts(forecasts));
}, []);
return (
<table>
{forecasts.map(forecast => (
<tr>
<td>{forecast.date.toLocaleDateString()}</td>
<td>{forecast.temperatureC}</td>
<td>{forecast.summary}</td>
</tr>
))}
</table>);
}
ここまで、ビルドは通るはずです。しかしいざ実行してみると、以下のエラーが発生することでしょう。
Uncaught TypeError: forecast.date.toLocaleDateString is not a function at...
つまり、上記 React のコンポーネント実装で下記となっているところ、
...
<td>{forecast.date.toLocaleDateString()}</td>
...
ここで toLocaleDateString
メソッド呼び出しが使えない、となっているのです。
これはどういうことかというと、TypeScript の型定義上は、WeatherForecast
型の date
プロパティは JavaScript の Date
型ということにはなっていますが、実際に実行時に JSON デシリアライズによって生成されるオブジェクト上は、date
プロパティは文字列のままだからなんですね。Date
型ではなく文字列には当然、toLocaleDateString
などという日時型特有のメソッドはありませんから、それでこのように実行時エラーとなってしまうわけです。
一般的な JavaScript/TypeScript プログラミングにおける JSON からオブジェクトへのデシリアライズ処理においては、TypeScript の型定義は使われないことと自分は認識しておりますが、そういうことで、上記のような実装は不可なんですね。もちろんそういった背景を踏まえて、JavaScript/TypeScript プログラミングにもこういった場合の流儀があるわけで、例えば REST API から取得するデータの型には、メンバーに日時型は使用しないこととして (下記)、
export type WeatherForecast = {
date: string, // 👈 意味上は Date がふさわしくても文字列型にする
temperatureC: number,
summary: string
}
その上で、日時を示す文字列を Date に変換して日付文字列に書式化して返す関数を定義し、これを使ってレンダリングするように実装する、などの実装手段が考えられると思います (下記)。
...
export const App = () => {
...
// 日時を示す文字列を、Date に変換して、日付文字列に書式化して返す関数を定義しておき...
const toLocaleDateString = (date:string) => new Date(date).toLocaleDateString();
...
return (
<table>
{forecasts.map(forecast => (
<tr>
<td>{toLocaleDateString(forecast.date)}</td>
{/*👆日時を日付文字列に書式化する関数呼び出しでくるむ */}
<td>{forecast.temperatureC}</td>
<td>{forecast.summary}</td>
</tr>
))}
</table>);
}
他にも fetch した直後の後処理で文字列から日時型への変換処理を仕込むなどの実装も想像されます。が、とにかく、REST API サーバーと JSON 形式でやりとりする際に、日時型のデータが含まれている場合は、JavaScript/TypeScript で実装する場合は何かしら仕込みというか考慮が必要である、と自分は理解しています。
私が Blazor を選ぶ理由
で、話は Blazor に戻るんですが、先に示した Blazor による実装の場合は、そのあたり深く考える必要なく、素で DateTime
型使っておけば、よしなに JSON からデシリアライズされますよね。 こういうときに自分は「あー、ここは Blazor のお陰で楽させてもらったなー」としみじみ感じています。
もちろんその背後では、System.Text.Json
によるデシリアライズ処理が猛烈に MSIL インタープリタ上で動いて仕事していたりと、コストはかかっています。ですが、そういうコストを払い、そういうレイヤーをひとつ載せることで、開発効率の向上や不具合遭遇のリスク低下といった恩恵を手に入れているわけです。
これこそが私が Blazor を選ぶ理由であり、JavaScript コードの山の上に築かれたものだとしても Blazor は無駄でも無意味でもない と考える理由です。
おわりに
以上、長々と持論を展開させて頂きました。
なお昨今のクライアント Web アプリケーション開発においては、JavaScript/TypeScript 以外の選択肢として Blazor (C#) のみならず、Rust や Dart をはじめ、様々なプログラミング言語や処理系、ライブラリやフレームワークが選べると認識しております。そういった中から私が Blazor を使っている背景には、長らく C# によるプログラミングに親しんできたという事情も大きく影響しているのは間違いないと思います。そのように、開発メンバーの技術セットの傾向や要件などによって、クライアント Web アプリケーション開発においてどのような言語・処理系がふさわしいかは、唯一無二の正解はないと思います。ですから誰もが自分の推しの言語・処理系を駆使して、世の中に素晴らしいプロダクトを産みだしていけば良いのだろうな、と思う今日この頃です。
最後にもうひとつ。
この投稿で示した「日時を含む JSON データを REST API サーバーから取得するとき」の JavaScript/TypeScript による実装例ですが、自分の技量では上記のような実装しか思い浮かびませんでしたものの、もっと上手なやり方があったりするのでしょうか、ぜひコメント欄などでお知らせ頂けるとありがたいです。
以上です!
Happy Coding ;)