ASP.NET Core で JavaScript サービスを使用してシングル ページ アプリケーションを作成する
作成者: Fiyaz Hasan
警告
この記事で説明されている機能は、ASP.NET Core 3.0 時点で互換性のために残されてます。 より単純な SPA フレームワーク統合メカニズムが Microsoft.AspNetCore.SpaServices.Extensions NuGet パッケージで利用できます。 詳細については、Microsoft.AspNetCore.SpaServices と Microsoft.AspNetCore.NodeServices の廃止に関するお知らせのページを参照してください。
シングル ページ アプリケーション (SPA) は、本質的に高度なユーザー エクスペリエンスを提供できることから、広く使われているタイプの Web アプリケーションです。 クライアント側の SPA フレームワークまたはライブラリ (Angular や React など) と、ASP.NET Core などのサーバー側のフレームワークの統合は、困難な場合があります。 JavaScript サービスは、統合プロセスの手間を減らすために開発されました。 これにより、さまざまなクライアントやサーバーのテクノロジ スタック間でシームレスな操作を行うことができます。
JavaScript サービスとは
JavaScript サービスは、ASP.NET Core のクライアント側テクノロジのコレクションです。 その目的は、ASP.NET Core を開発者の SPA 構築に有用なサーバー側プラットフォームとして位置付けることにあります。
JavaScript サービスは、次の 2 つの異なる NuGet パッケージで構成されています。
- Microsoft.AspNetCore.NodeServices (NodeServices)
- Microsoft.AspNetCore.SpaServices (SpaServices)
これらのパッケージは、次のシナリオで役立ちます。
- サーバーで JavaScript を実行する
- SPA フレームワークまたは SPA ライブラリを使用する
- Webpack を使用してクライアント側の資産を構築する
この記事では、SpaServices パッケージを使用することに重点を置いて説明します。
SpaServices とは
SpaServices は、ASP.NET Core を開発者の SPA 構築に有用なサーバー側プラットフォームとして位置付けるために作成されました。 SpaServices は、ASP.NET Core で SPA を開発する際に必ず必要になるのものではありません。また、SpaServices を使っても、開発者が特定のクライアント フレームワークにロックされることはありません。
SpaServices は、次のような便利なインフラストラクチャを提供します。
これらのインフラストラクチャ コンポーネントを集めると、開発ワークフローとランタイム エクスペリエンスの両方を強化することができます。 このコンポーネントは、個別に採用できます。
SpaServices を使用するための前提条件
SpaServices を使用するには、以下をインストールします。
Node.js (バージョン 6 以降) と npm
これらのコンポーネントがインストールされていて検出可能であることを確認するには、コマンド ラインから次のコマンドを実行します。
node -v && npm -v
Azure の Web サイトにデプロイする場合、操作は必要ありません。サーバー環境に Node.js がインストールされて使用できるようになります。
-
- Visual Studio 2017 を使用している Windows では、 .NET Core クロスプラットフォーム開発ワークロードを選択すると SDK がインストールされます。
Microsoft.AspNetCore.SpaServices NuGet パッケージ
サーバー側の事前レンダリング
ユニバーサル (アイソモーフィックとも呼ばれます) アプリケーションは、サーバーとクライアントの両方で実行できる JavaScript アプリケーションです。 Angular、React などの一般的なフレームワークは、このアプリケーション開発スタイル用のユニバーサル プラットフォームを提供します。 まず、サーバーで Node.js を使用してフレームワーク コンポーネントをレンダリングし、続く処理の実行をクライアントに委任します。
SpaServices が提供する ASP.NET Core タグ ヘルパーは、サーバーで JavaScript 関数を呼び出すことで、サーバー側の事前レンダリングの実装が簡単にします。
サーバー側の事前レンダリングの前提条件
aspnet-prerendering npm パッケージをインストールします。
npm i -S aspnet-prerendering
サーバー側の事前レンダリングの構成
プロジェクトの _ViewImports.cshtml
ファイルに名前空間を登録すると、タグ ヘルパーを検出できるようになります。
@using SpaServicesSampleApp
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@addTagHelper "*, Microsoft.AspNetCore.SpaServices"
タグ ヘルパーは、Razor ビューで HTML に似た構文を利用することで、低レベルの API と直接通信する複雑な部分を抽象化します。
<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>
asp-prerender-module タグ ヘルパー
前のコード例で使用されている asp-prerender-module
タグ ヘルパーは、サーバーの Node.js で ClientApp/dist/main-server.js
を実行します。 わかりやすくするために、main-server.js
ファイルは、Webpack ビルド プロセスで TypeScript から JavaScript へのトランスパイル タスクの成果物とします。 Webpack では main-server
のエントリ ポイントの別名が定義されています。また、この別名の依存関係グラフの走査は、ClientApp/boot-server.ts
ファイルから開始します。
entry: { 'main-server': './ClientApp/boot-server.ts' },
次の Angular の例では、ClientApp/boot-server.ts
ファイルが aspnet-prerendering
npm パッケージの createServerRenderer
関数と RenderResult
型を使用して Node.js によるサーバー レンダリングを構成しています。 サーバー側のレンダリング用の HTML マークアップは、resolve 関数呼び出しに渡されます。これは、厳密に型指定された JavaScript の Promise
オブジェクトでラップされています。 Promise
オブジェクトは、DOM のプレースホルダー要素に注入する HTML マークアップを非同期にページに渡すという重要な役割を果たしています。
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
export default createServerRenderer(params => {
const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];
return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);
return new Promise<RenderResult>((resolve, reject) => {
zone.onError.subscribe(errorInfo => reject(errorInfo));
appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: state.renderToString()
});
moduleRef.destroy();
});
});
});
});
});
asp-prerender-data タグ ヘルパー
asp-prerender-data
タグ ヘルパーと asp-prerender-module
タグ ヘルパーと組み合わせると、Razor ビューからサーバー側 JavaScript にコンテキスト情報を渡すことができます。 たとえば、次のマークアップは、ユーザー データを main-server
モジュールに渡します。
<app asp-prerender-module="ClientApp/dist/main-server"
asp-prerender-data='new {
UserName = "John Doe"
}'>Loading...</app>
渡された UserName
引数は、組み込みの JSON シリアライザーによってシリアル化され、params.data
オブジェクトに格納されます。 次の Angular の例では、このデータを使用して、h1
の要素内に独自の挨拶文を作成します。
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
export default createServerRenderer(params => {
const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];
return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);
return new Promise<RenderResult>((resolve, reject) => {
const result = `<h1>Hello, ${params.data.userName}</h1>`;
zone.onError.subscribe(errorInfo => reject(errorInfo));
appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: result
});
moduleRef.destroy();
});
});
});
});
});
タグ ヘルパーで渡されるプロパティ名は、パスカルケース表記で表されます。 同じプロパティ名がキャメルケースで表される JavaScript とは対照的です。 既定の JSON シリアル化構成が、この違いに対応します。
上のコード例を発展させると、サーバーからビューにデータを渡すことができます。そのためには、resolve
関数に渡す globals
プロパティを設定します。
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
export default createServerRenderer(params => {
const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];
return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);
return new Promise<RenderResult>((resolve, reject) => {
const result = `<h1>Hello, ${params.data.userName}</h1>`;
zone.onError.subscribe(errorInfo => reject(errorInfo));
appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: result,
globals: {
postList: [
'Introduction to ASP.NET Core',
'Making apps with Angular and ASP.NET Core'
]
}
});
moduleRef.destroy();
});
});
});
});
});
globals
オブジェクト内で定義されている postList
配列は、ブラウザーのグローバル window
オブジェクトにアタッチされます。 この変数はグローバル スコープに含められ、それによって作業の重複がなくなります。具体的に言えば、同じデータをサーバーで一度、クライアントで再度読み込むことがなくなるためです。
Webpack Dev ミドルウェア
Webpack Dev ミドルウェア は、Webpack が必要に応じてリソースを構築することで、合理化された開発ワークフローを実現します。 ページがブラウザーに再読み込みされると、ミドルウェアはクライアント側のリソースを自動的にコンパイルして提供します。 別の方法として、サードパーティの依存関係またはカスタム コードが変更されたときに、プロジェクトの npm ビルド スクリプトを介して Webpack を手動で呼び出す方法もあります。 次の例では、package.json
ファイル内の npm ビルド スクリプトを示しています。
"build": "npm run build:vendor && npm run build:custom",
Webpack Dev ミドルウェアの前提条件
aspnet-webpack npm パッケージをインストールします。
npm i -D aspnet-webpack
Webpack Dev ミドルウェアの構成
Webpack Dev ミドルウェアは、Startup.cs
ファイルの Configure
メソッドにある次のコードによって、HTTP 要求パイプラインに登録されます。
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
// Call UseWebpackDevMiddleware before UseStaticFiles
app.UseStaticFiles();
UseStaticFiles
拡張メソッドを使用して静的ファイル ホスティングを登録する前に、UseWebpackDevMiddleware
拡張メソッドを呼び出す必要があります。 セキュリティ上の理由から、アプリが開発モードで実行されている場合にのみ、ミドルウェアを登録してください。
webpack.config.js
ファイルの output.publicPath
プロパティは、dist
フォルダーの変更を監視するようにミドルウェアに指示します。
module.exports = (env) => {
output: {
filename: '[name].js',
publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
},
ホット モジュール置換
Webpack のホット モジュール置換 (HMR) 機能は、Webpack Dev ミドルウェアが進化したものと考えることができます。 HMR は同じ利点をすべて実現しますが、変更をコンパイルした後にページ コンテンツを自動的に更新するので、開発ワークフローがさらに効率化されます。 これをブラウザーの更新と混同しないでください。ブラウザーの更新は、現在のメモリ内の状態と SPA のデバッグ セッションに影響します。 Webpack Dev ミドルウェア サービスとブラウザーの間には、ライブ リンクがあります。これは、変更がブラウザーにプッシュされることを意味します。
ホット モジュール置換の前提条件
webpack-hot-middleware npm パッケージをインストールします。
npm i -D webpack-hot-middleware
ホット モジュール置換の構成
HMR コンポーネントは、Configure
メソッドで MVC の HTTP 要求パイプラインに登録する必要があります。
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true
});
Webpack Dev ミドルウェアの場合と同様に、UseWebpackDevMiddleware
拡張メソッドを UseStaticFiles
拡張メソッドの前に呼び出す必要があります。 セキュリティ上の理由から、アプリが開発モードで実行されている場合にのみ、ミドルウェアを登録してください。
webpack.config.js
ファイルでは、plugins
配列を定義する必要があります。これは空のままでもかまいません。
module.exports = (env) => {
plugins: [new CheckerPlugin()]
ブラウザーでアプリを読み込むと、開発者ツールのコンソールのタブに HMR がアクティブ化されたことの確認が表示されます。
ルーティング ヘルパー
ほとんどの ASP.NET Core ベースの SPA では、サーバー側のルーティングに加えて、クライアント側のルーティングが必要になります。 SPA および MVC のルーティング システムは、干渉せずに個別に動作させることができます。 しかし、1 つのエッジケースでは、HTTP 応答 404 を認識するという課題があります。
拡張子のない /some/page
というルートを使用するシナリオを考えてみましょう。 要求がサーバー側ルートとはパターン一致せず、クライアント側のルートと一致すると仮定します。 ここで、/images/user-512.png
の受信要求について考えてみます。通常は、サーバー上のイメージ ファイルを検索する必要があります。 要求されたリソース パスがどのサーバー側ルートまたは静的ファイルとも一致しない場合、クライアント側アプリケーションがこれを処理することはほとんどありません。通常は、HTTP 状態コード 404 が返されます。
ルーティング ヘルパーの前提条件
クライアント側ルーティング npm パッケージをインストールします。 Angular を使用する場合の例を示します。
npm i -S @angular/router
ルーティング ヘルパーの構成
Configure
メソッドで MapSpaFallbackRoute
という名前の拡張メソッドを使用します。
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
ルートは、構成された順序で評価されます。 そのため、上のコード例の default
ルートは、最初にパターン マッチングに使用されます。
新しいプロジェクトを作成する
JavaScript サービスには、事前構成済みのアプリケーション テンプレートが用意されています。 これらのテンプレートでは、Angular、React、Redux などのさまざまなフレームワークやライブラリと共に SpaServices を使用します。
これらのテンプレートは、次のコマンドを実行して .NET CLI 経由でインストールできます。
dotnet new --install Microsoft.AspNetCore.SpaTemplates::*
使用可能な SPA テンプレートの一覧が表示されます。
テンプレート | 短い形式の名前 | 言語 | Tags |
---|---|---|---|
MVC ASP.NET Core with Angular | angular | [C#] | Web/MVC/SPA |
MVC ASP.NET Core with React.js | react | [C#] | Web/MVC/SPA |
MVC ASP.NET Core with React.js and Redux | reactredux | [C#] | Web/MVC/SPA |
SPA テンプレートの 1 つを使用して新しいプロジェクトを作成するには、dotnet new コマンドでテンプレートの短い形式の名前を指定します。 次のコマンドは、サーバー側用に構成された ASP.NET Core MVC を使用して、Angular アプリケーションを作成します。
dotnet new angular
ランタイム構成モードの設定
2 つのプライマリ ランタイム構成モードがあります。
- 開発:
- デバッグを容易にするソース マップが含まれています。
- パフォーマンス向上のためにクライアント側のコードを最適化することはしません。
- 実稼働:
- ソース マップを除外します。
- バンドルと縮小を使用して、クライアント側のコードを最適化します。
ASP.NET Core では、ASPNETCORE_ENVIRONMENT
という名前の環境変数を使用して構成モードを格納します。 詳細については、「環境を設定する」を参照してください。
.NET CLI で実行する
プロジェクト ルートで次のコマンドを実行して、必要な NuGet パッケージと npm パッケージを復元します。
dotnet restore && npm i
アプリケーションをビルドして実行します。
dotnet run
アプリケーションは、ランタイム構成モードに従って、localhost で開始されます。 ブラウザーで http://localhost:5000
に移動すると、ランディング ページが表示されます。
Visual Studio 2017 で実行する
dotnet new コマンドで生成された .csproj
ファイルを開きます。 必要な NuGet および npm パッケージは、プロジェクトを開いたときに自動的に復元されます。 この復元プロセスには数分かかる場合があり、これが完了するとアプリケーションを実行する準備ができています。 緑色の実行ボタンをクリックするか Ctrl + F5
キーを押すと、ブラウザーが開いてアプリケーションのランディング ページが表示されます。 アプリケーションは、ランタイム構成モードに従って、localhost で実行されます。
アプリのテスト
SpaServices テンプレートは、Karma と Jasmine を使用してクライアント側のテストを実行するように事前構成されています。 Jasmine は JavaScript 用の一般的な単体テスト フレームワークであるのに対し、Karma はそれらのテストのテスト ランナーです。 Karma は Webpack Dev ミドルウェアと連携するように構成されています。これにより、開発者は、変更が行われるたびにテストを停止して実行する必要がなくなります。 テスト ケースまたはテスト ケース自体に対してコードが実行されているかどうかにかかわらず、テストは自動的に実行されます。
例として、Angular アプリケーションを使用します。counter.component.spec.ts
ファイルの CounterComponent
に対し、2 つの Jasmine テスト ケースが既に提供されています。
it('should display a title', async(() => {
const titleText = fixture.nativeElement.querySelector('h1').textContent;
expect(titleText).toEqual('Counter');
}));
it('should start with count 0, then increments by 1 when clicked', async(() => {
const countElement = fixture.nativeElement.querySelector('strong');
expect(countElement.textContent).toEqual('0');
const incrementButton = fixture.nativeElement.querySelector('button');
incrementButton.click();
fixture.detectChanges();
expect(countElement.textContent).toEqual('1');
}));
ClientApp ディレクトリで、コマンド プロンプトを開きます。 次のコマンドを実行します。
npm test
このスクリプトは、Karma テスト ランナーを起動します。これにより、karma.conf.js
ファイルで定義されている設定が読み取られます。 その他の設定として、karma.conf.js
の files
配列で実行するテスト ファイルを指定できます。
module.exports = function (config) {
config.set({
files: [
'../../wwwroot/dist/vendor.js',
'./boot-tests.ts'
],
アプリの発行
Azure への発行の詳細については、こちらの GitHub の問題のページを参照してください。
生成されたクライアント側アセットと発行された ASP.NET Core 成果物をすぐにデプロイ可能なパッケージにまとめると、煩雑になることがあります。 ありがたいことに、SpaServices は、RunWebpack
という名前のカスタム MSBuild ターゲットを使用して、発行プロセス全体を調整します。
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec Command="npm install" />
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
この MSBuild ターゲットには、次の役割があります。
- npm パッケージを復元する
- サードパーティ製クライアント側アセットの実稼働レベルのビルドを作成する
- カスタムのクライアント側アセットの実稼働レベルのビルドを作成する
- Webpack が生成したアセットを発行フォルダーにコピーする
次を実行すると、MSBuild ターゲットが呼び出されます。
dotnet publish -c Release
その他の技術情報
ASP.NET Core