SPAテンプレートを利用する
第8回の冒頭で触れたように、Web APIはReactやAngularなどのSPA(Single Page Application)と組み合わせての利用が想定されます。これを受けて、ASP.NET CoreにはSPA利用のためのテンプレートも用意されており、それらを利用することでクライアントサイド(SPA)とサーバサイド(Web API)の双方を一度に生成して利用することができるようになっています。ここでは、このうちReactによるSPA開発を紹介します。
プロジェクトの作成と実行
Reactをフロントエンドに用いる開発では、テンプレートにreactを指定してdotnet newコマンドを実行します。指定されているオプションは、第8回でWeb APIの生成に用いたものと同じです。
% dotnet new react -o ReactApp --no-https -f net7.0
なお、これまで同様にdotnet new --listコマンドでの出力を見ると、reactテンプレートは「react」は「React.js での ASP.NET Core」に相当し、言語はC#で、タグには「Web/MVC/SPA」が指定されています。これで、クライアントサイドとサーバサイドが一度に生成されます。双方が、一つのdotnet runコマンドあるいはdotnet watchコマンドで実行できます。
% dotnet watch dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload. 💡 Press "Ctrl + R" to restart. dotnet watch 🔧 Building... …略…
これで、以下の図3のページが最初に表示され、しばらく経つと本来のSPAのトップページ(図4)が表示されます。
図4のページ上部には「ReactApp」と表示されていますが、右端に3つのリンクがあります。これは、第7回でも見てきたようなホーム(Home)、カウンタ(Counter)、気象情報(Fetch data)のページへのリンクに相当します。それぞれクリックすると、「Hello, world!」と表示されているペインがそれぞれ以下のように変化します(図5~図6「Home」は同じページになるだけなので省略)。このとき、ページの移動に伴いURLも変化していることに注意してください。
ページの内容の変化はReactによるものですが、Fetch dataページではWeb APIとして構成されたバックエンドからデータを取得し、表示しています。
SPAテンプレートの動作モデル
SPAテンプレートで生成されるアプリケーションは、公開時と開発時で異なる動作モデルとなっています。公開時は、図7のような構成です。
SPAはデプロイ時に、ソースファイルなどがコンパイル・バンドルされて、静的なファイル(index.htmlやJavaScriptファイル)としてサーバのwwwrootフォルダ(静的なファイルを配置して公開するためのフォルダ)に配置されます。SPAが起動するときのリクエストは、静的なファイルのレスポンスとして返り、Webブラウザで実行されます。SPAによるAPIへのリクエストは、Web API単体と同じくサーバで処理されてレスポンスとして返り、SPAの表示などに反映されます。これに対し、開発時は図8のような構成です。
SPAのファイルは、Node.js上で稼働する開発サーバが提供します。SPAが起動するときのリクエストは、SPAプロキシと呼ばれるミドルウェアが開発サーバにリダイレクトし、開発サーバからのレスポンスとして返り、Webブラウザで実行されます。SPAによるAPIへのリクエストは、そのまま透過的にサーバで処理されてレスポンスとして返り、SPAの表示などに反映されます。このように、SPAが公開時のように静的なファイルとして構成されていなくても、Webブラウザからアクセスして起動することができます。
上記の動作検証において、サーバへのリクエスト(ここではhttp://localhost:5111/)でいったん図3のページが表示されました。これはSPAプロキシが用意したページで、このページが表示されている間に開発サーバを起動し、起動したら開発サーバのURL(ここではhttp://localhost:44494/)にリダイレクトします。リダイレクトの結果、図4のSPAトップページが表示されたわけです。
プロジェクトの構成とファイル
動作モデルも踏まえて、reactテンプレートによって生成されたプロジェクトを見てみましょう。プロジェクトは、コントローラベースのWeb APIアプリケーションに存在したControllers、Program.csなどのフォルダやファイルに加えて、ClientApp、Pagesといったフォルダから構成されます。Controllers、Program.csはWeb APIのためのフォルダとファイルですが、SPAとともにホスティングするためにその内容は若干異なったものになっています。これらをはじめとして、Web API単独とは異なるファイルについて、まずは見ていきます。
ReactApp.csprojファイルを確認する
Web API単独の場合の.csprojファイルは、ターゲットの.NETバージョンを明示するなどのシンプルなものでした。reactテンプレートでは、SPAやSPAプロキシに関する設定(1)や、SPAプロキシに対する参照の設定(2)、SPAの公開に関する設定(3)、この他、ビルド時や公開時の設定(4)(5)が細かく記述されています。
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> (1)
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>
<SpaRoot>ClientApp\</SpaRoot>
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
<SpaProxyServerUrl>http://localhost:44494</SpaProxyServerUrl> (1')
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup> (2)
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.0" />
</ItemGroup>
<ItemGroup> (3)
<Content Remove="$(SpaRoot)**" />
…略…
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> (4)
…略…
</Target>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish"> (5)
…略…
</Target>
</Project>
上記で、最初にアクセスしたページからReactページにリダイレクトされましたが、これは(1')によるものです。Microsoft.AspNetCore.SpaProxyでは、SPAプロキシを起動するロジックを自動的にアプリケーションに挿入するので、アプリケーションがSPAプロキシ起動のためのコードを記述する必要がありません。
コントローラを確認する
コントローラファイルについても見てみます。
…略… [HttpGet] (1) public IEnumerable<WeatherForecast> Get() …略…
異なるのは(1)の箇所のみです。HTTP GETメソッドによるアクション省略時に呼び出されるメソッドですが、name属性が省略されています。これは、後述しますがSwaggerサポートが省略されていることで、OASのoperationId要素へのひも付けが不要だからです。既定のコントローラの動作としては全く同じです。
Program.csファイルを確認する
Swaggerサポートが省略されるなど、Web API単体のときとProgram.csファイルの内容が大きく異なります。
…略…
var builder = WebApplication.CreateBuilder(args); (1)
builder.Services.AddControllersWithViews();
var app = builder.Build(); (2)
if (!app.Environment.IsDevelopment())
{
}
app.UseStaticFiles(); (3)
app.UseRouting(); (4)
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html");;
app.Run();
(1)ではビルダーオブジェクトを生成していますが、Swaggerジェネレータの追加はありません。コントローラ利用の追加も、AddControllersメソッドではなくAddControllersWithViewsメソッドとなっています。AddControllersWithViewsメソッドは、単なるコントローラではなくビューを使ったコントローラを追加します。これは、一部ビューを用いる構成になっているためです。
(2)ではアプリケーションオブジェクトを生成していますが、DebugターゲットにおいてもSwagger UIなどのミドルウェア有効化のコードはありません。
(3)のUseStaticFilesメソッドは、wwwrootフォルダ以下にある静的なファイルを使用するためのものです。既述の通り、公開時のSPAのファイルはwwwrootフォルダに置かれるので、そのために必要です。
(4)のUseRoutingメソッドとMapControllerRouteメソッドは、ルーティングを有効にして単一ルートを設定しています。このルートはデフォルトのルールとして利用されます。
(5)のMapFallbackToFileメソッドは、ルートに一致するページが存在しない場合に表示するファイルを指定しています。指定されているindex.htmlは、SPAのルートとなるファイルです。
Pagesフォルダを確認する
Pagesフォルダには、Error.cshtmlなどといったエラーメッセージ表示のためのビューが置かれます。これらのファイルについては、第5回や第7回で触れました。reactテンプレートでは、SPAプロキシの起動に失敗したときなどのために、内部的に呼び出されます。ここのビューのために、Program.csファイルではAddControllersメソッドではなくAddControllersWithViewsメソッドが呼び出されました。
ClientAppフォルダを確認する
ClientAppフォルダには、クライアントアプリケーションのコードが収納されます。フォルダ内の構成は、Create-React-App(Reactアプリケーション作成のためのプロジェクト)のテンプレートに準じたものとなっています。バンドラーによるビルドを想定し、publicフォルダに静的なファイル、srcフォルダにReactコンポーネントなどのJavaScriptファイルが配置されます。本稿はReactアプリの構造を解説することが目的ではありませんが、Web APIを利用するSPAとしての動作がどうなっているかに絞って見てみます。まず、以下のファイルの役割を押さえておきましょう。
- ClientApp/public/index.html……アプリケーションのルート。id属性が"root"であるdiv要素が置かれる
- ClientApp/src/index.js……上記のdiv要素にレンダリングする内容。Appコンポーネントが置かれる
- ClientApp/public/AppRoute.js……SPAにおけるルーティングの設定。URLパスとReactコンポーネントの対応が置かれる
- ClientApp/src/App.js……Appコンポーネントの内容。ルートに対応するコンポーネントを展開する
- ClientApp/src/components/FetchData.js……URLパス「/fetch-data」に対応するFetchDataコンポーネントの内容
Web APIから見た場合に重要なのは、最後のFetchData.jsです。これは、「/fetch-data」すなわち気象情報のページに相当するコンポーネントです。このファイルの一部を抜粋します。
…略…
componentDidMount() { (1)
this.populateWeatherData();
}
…略…
async populateWeatherData() { (2)
const response = await fetch('weatherforecast');
const data = await response.json();
this.setState({ forecasts: data, loading: false });
}
(1)は、コンポーネントのマウント後に実行される処理で、(2)で定義されているAPI呼び出しの関数populateWeatherDataを呼び出しています。これにより、ページに必要な気象情報のデータが準備されます。API呼び出しは、fetchメソッドによって非同期に行い、戻り値のJSONデータでコンポーネントのstateオブジェクトを更新しています。更新後に実行されるレンダリングにより、JSONデータを用いて気象情報の表が作成されます。
まとめ
今回は、.NET 6からサポートされた最小限のWeb APIであるMinimal APIと、これを含めたWeb APIを利用するSPAのテンプレートを紹介しました。
次回は、gRPCを使用したアプリケーションの開発を紹介します。
