これは 「祝 .NET 6 GA!.NET 6 での開発 Tips や試してみたことなど、あなたの「いち推し」ポイントを教えてください【PR】日本マイクロソフト Advent Calendar 2021」 の19日目の記事です。
もうすぐ25日ですね、引き続き楽しんでいきましょう。
さて、カレンダーのエントリ時には「Blazor WebAssemblyとNativeについて書くかも」としていましたが、作っているネタの進捗が悪いので、別のアツい.NET 6 + Blazorネタをご紹介します。
PreviewやRCのリリース時にも面白いネタをガンガン投稿してくれる .NET Blog ですが、「ASP.NET Core updates in .NET 6 Release Candidate 1」の投稿時にも気になる内容がありました。
そう、 Render Blazor components from JavaScript
、これを使用した一例の Generate Angular and React components using Blazor
の項目です。
大まかな内容としては以下のとおりです。
- 既存のJavaScriptアプリケーションとBlazorコンポーネントとの統合の強化
- JavaScriptアプリケーションには例えばAngularやReactなどで作られたアプリケーションも含まれる!
- BlazorコンポーネントをJavaScriptからコンテナ要素にマウント可能
- Experimentalでサンプルレベルではあるけれども、既存のBlazorコンポーネントを呼び出すためのAngular、React用のテンプレートコードを吐き出すプロジェクトの公開
昨年の Blazorのアドベントカレンダー で Blazor WebAssemblyをただC#実行プラットフォームとして使って既存のReactのWebアプリを拡張した話 と言ったタイトルでReactアプリケーションとBlazor WebAssemblyの共存に関する記事を書きましたが、このユースケースよりも更に高度なシナリオを解決するものが公式からリリースされていました。
今回はこちらを軽く触れ、ちょっとした応用事例をご紹介します。
JavaScript component generation sample を眺める
.NET Blogで紹介されているのは
のリポジトリで、そのままサンプルを動かすだけであればこれで十分なので、このサンプルをまずは眺めていきましょう。
このサンプルの構造は以下のようになっています。
.
├── BlazorAppGeneratingJSComponents <- Blazor WebAssemblyアプリ、生成対象のBlazor Componentが含まれる
├── JSComponentGeneration <- Blazor WebAssemblyアプリから参照されるAttributesやヘルパ関数が含まれている
├── JSComponentGeneration.Build <- メタデータなどから実際にBlazor ComponentをJavaScriptから呼び出すためのJSコードを吐き出すコード、及びタスクの定義が含まれている
├── JSComponentGeneration.Shared <- ヘルパ関数、気にしなくて良い
├── JSComponentGeneration.sln
├── README.md
├── angular-app-with-blazor <- Blazor Componentを呼び出すAngularのサンプルプロジェクト、今回の説明では触れない
└── react-app-with-blazor <- Blazor Componentを呼び出すReactのサンプルプロジェクト、今回の説明の対象
実際に動作させる場合はREADMEに沿って行えば良いのですが、実際はこれだけでは動作しません。
READMEの内容を行う前に
$ cd JSComponentGeneration.Build
$ dotnet build; dotnet build -c Release
でコード生成を行うコードのビルドを行いましょう。
この準備が整った状態でREADMEに沿ってビルドを行い、ホストしているURLを眺めてみましょう。
素晴らしいですね、 BlazorAppGeneratingJSComponents/Counter.razor
で実装されているコンポーネントがReactアプリケーション内で動作しています!
さて、これはどの様にして実現されているのでしょうか?
ビルドから実行までの流れと共に追ってみましょう。
Blazor WebAssemblyアプリケーションをビルドしたタイミング
さてビルド時に何が起こっているのでしょうか?
BlazorAppGeneratingJSComponents
のプロジェクト設定であるBlazorAppGeneratingJSComponents.csproj
を見てみると、以下のような記述があります。
<Import Project="..\JSComponentGeneration.Build\build\netstandard2.0\JSComponentGeneration.build.targets" />
何かしらビルド時に下記のtargetsファイルに書かれたタスクを走らせようとしていることがわかります。
それではその中身を見てみます。
<Project>
<PropertyGroup>
<_TaskDll>$(MSBuildThisFileDirectory)JSComponentGeneration.Build.dll</_TaskDll>
...
</PropertyGroup>
<Target Name="HackRazorSdk">
<ItemGroup>
<!-- Add RazorComponent files to the set of files to be code-gened. -->
<RazorGenerateWithTargetPath Include="@(RazorComponentWithTargetPath)" />
</ItemGroup>
...
</Target>
...
<UsingTask TaskName="GenerateReactComponents" AssemblyFile="$(_TaskDll)" />
<Target Name="GenerateJavaScriptComponents" AfterTargets="CopyFilesToOutputDirectory" DependsOnTargets="HackRazorSdk;ResolveTagHelperRazorGenerateInputs">
<GenerateReactComponents OutputPath="$(OutputPath)" IntermediateOutputPath="$(IntermediateOutputPath)" AssemblyName="$(AssemblyName)" JavaScriptComponentOutputDirectory="..\react-app-with-blazor\src" />
</Target>
</Project>
このtargetsファイルの中身は
-
GenerateReactComponents
のタスクを行うための依存などを定義している - 実際の処理は
JSComponentGeneration.Build.dll
(最初にビルドしてもらったファイルです)に定義されている - このタスクは
HackRazorSdk;ResolveTagHelperRazorGenerateInputs
の2つのタスクに依存している - https://github.com/dotnet/sdk/blob/f22b9c002b17d54d4f48218007b90e215278b433/src/RazorSdk/Targets/Microsoft.NET.Sdk.Razor.CodeGeneration.targets の辺りのタスクを明示的に呼び出すためのハック
みたいなことが書かれています。
JSComponentGeneration.Buildは何をしているのか
ざっくり説明すると
-
HackRazorSdk;ResolveTagHelperRazorGenerateInputs
のタスクで生成されたobj/Debug/net6.0/BlazorAppGeneratingJSComponents.TagHelpers.output.cache
のファイルをパース - パース結果からComponentの公開PropertyやEventCallback、型を取得
- それを元にReactのコードを生成
みたいなことをしています。
パースした*.TagHelpers.out.cache
(実質jsonファイル)にはビルド対象となったコンポーネントなどの情報が数多く含まれています(ぜひとも眺めてみてください)。
このパースした結果をもとにJSComponentGeneration.Build/React/ReactComponentWriter.cs
でテンプレートに合わせてReactのコードを生成するのです。
ビルド対象となるコンポーネントの判定にGenerateReactAttribute
の存在を確認しているが、その判定のためにビルド済みAssemblyを読み込んで…みたいな処理もあったりするのですが、詳しい処理を知りたい場合はぜひコードを見に行ってください。
上記のtargetsの流れを踏まえてざっくり以下の2工程でコードが生成されていることがわかります。
- Blazor WebAssemblyのアプリケーションのビルド
- ビルド生成物やビルド時のメタデータからJSComponentGeneration.BuildがReactのコードを生成
Reactアプリケーションの実行タイミング
まずはReactアプリケーションを実行するファイルを読み込むindex.htmlから見ていきます。
index.htmlには
<script src="_framework/blazor.webassembly.js"></script>
の記述があり、Blazor WebAssemblyアプリケーションを実行する際に必要な処理を行っています。
ここで実行されるBlazor WebAssemblyのコードはどの様になっているか見るために Program.cs
を確認すると
builder.RootComponents.RegisterForReact<Counter>();
普段は見慣れない RegisterForReact
というものが増えていることが確認できます。
この実装は JSComponentGeneration/React/JSComponentConfigurationExtensions.cs
にあり、RegisterForJavaScript
の呼び出しをラップしているだけの関数ということがわかります。
RegisterForJavaScript
のコードは
https://github.com/dotnet/aspnetcore/blob/715b8dba5e096a134a0c232aacac9640eb5782e5/src/Components/Web/src/JSComponents/JSComponentConfigurationExtensions.cs
にあり、historyをたどってみると
上記のPRにたどり着きます。
どうもこのメソッドで登録されたコンポーネントは動的なRootComponentになることが可能で、またJavaScriptからもインスタンス化が可能になるようです。
そう、これこそがこの記事の最初に挙げた Render Blazor components from JavaScript
の正体なのです。
話をまとめると、Reactアプリケーションの実行前にBlazor WebAssemblyの初期化を行っていて、その中ではReact側からインスタンス化するBlazor Componentの登録を行っていることがわかります。
それではReact側は何をしているのかというと、特に変わったことはありません。
生成された
import { useBlazor } from './blazor-react';
export function Counter({
title,
incrementAmount,
customObject,
customCallback,
}) {
const fragment = useBlazor('counter', {
title,
incrementAmount,
customObject,
customCallback,
});
return fragment;
}
このCounterコンポーネントを呼び出すだけなのです。
その裏側ではテンプレートである useBlazor
hookを使用し Blazor.rootComponents.add
メソッドを使い、マウントするコンテナ要素とインスタンス化するBlazor Componentを指定したりと、様々な処理が行われていますが、基本的には気にする必要はありません。
さてざっくりですがこれでビルド時に何が起こってコードが生成されて、どの様な仕組みでReactからBlazor Componentが呼び出されているのかがつかめたかと思います。
この流れを掴んだところで、自分が手を入れられそうな箇所を考えてみましょう。
例えばComponentを動的RootComponentに登録する箇所や、ReactからBlazor Componentをインスタンス化するuseBlazor
の中身は変える必要は無いように思えます。
そうすると、実際にコードを生成する JSComponentGeneration.Build
が遊べそうな感じがしてきますね?
ということで、応用としてJSComponentGeneration.Buildを改変してみましょう
JSComponentGeneration.Buildを改変し、生成されるReactコードをTypeScript化する
ReactアプリケーションもTypeScriptで開発することが出来ます。
TypeScriptで開発することで、Propsに誤った型のObjectを渡したり、また必要なプロパティを渡し忘れたりと多くのメリットがあります。
しかしながら今回のsampleでは、JavaScriptで書かれたReactのコードが生成されます。
せっかくBlazor WebAssemblyのアプリケーションはC#で開発し、型が存在するのにもったいないです。
C#の型をどうにかして生成されるReactのコードに反映させてみましょう。
実際に出来上がったコードは
こちらで公開しています。
差分だけをシュッと見たい場合は
こちらからご覧になれます。
………と、説明を始めようと思いましたがもりもりな内容になってしまいそうなのでかいつまんで説明します。
Reactのコード生成はJavaScriptですが、sampleにあるAngularのプロジェクトではTypeScriptを使用しています。
そのため、Angularのコード生成に使用しているファイルをよしなにコピペすると完成します。
またこのまま生成してもuseBlazor
の型が特定できないためtsconfigの設定次第ではビルドが通りません。
ということで雑に型をつけてしましまいます。
これをするだけでTypeScriptなReactのコード生成が行えてしまうのです。
おわりに
本記事では Generate Angular and React components using Blazor
を試すことが出来るサンプルリポジトリの紹介と、中身の大まかな説明、またその応用事例について触れました。
応用事例の箇所では、オレオレテンプレートを用いて、生成されるコードの調整が行えることを紹介しました。
今回説明した内容以外にも、.NET 6では数多くのJavaScript相互運用の機能拡張だったりパフォーマンスの改善が行われています。
この記事でBlazorに興味を持った方がいましたら、ぜひとも Blazor Advent Calendar 2021 もチェックしてみてください。
最後に少し宣伝にはなりますが、このサンプルコードをベースに、より複雑なユースケースで開発を行ったサンプルを
で公開しています。
このリポジトリや、実際にBlazor WebAssemblyとReactを相互に使うアプリケーションを作る上での流れを解説した本をC99で頒布予定です。
ご興味ありましたらぜひお立ち寄りください。