対象読者
- 新しいASP.NET Coreの機能について知りたい方
- MacやLinuxなどでASP.NET Coreアプリケーションを動かしたい方
検証環境
本稿では、以下の環境で動作を確認しています。
- macOS Sierra 10.12.6
- .NET Core 2.0
- Node.js v7.10.1
- npm v4.2.0
ASP.NET CoreとSPAフレームワークの連携箇所を確認しよう(クライアントサイド)
SPAの実装となるクライアント側のコード(HTML、CSS、JavaScript等)は、ClientAppディレクトリに配置されています。AngularやReactなどのSPAフレームワークを使ったコードはここに、各SPAフレームワークのコーディングルールに従って実装していくことになります。今回、SPAフレームワークとして選択したAngularの詳しい内容については、こちらの連載も参考にしてください。
ClientAppのディレクトリ構成
以下はClientApp配下のディレクトリ構造で、サーバサイドレンダリングに関連するものを抜粋したものです。これらはプロジェクトテンプレートからプロジェクトを作成した際にあらかじめ用意されているものですが、それぞれどのような役割を担っているのかをコードを見ながら確認していきます。
/ClientApp ┣ boot.server.ts ・・・Angularでサーバサイドレンダリングを行うための設定と ┃ ルートモジュール(AppModule)を指定したファイル ┣ /app ┃ ┣ app.module.server.ts(AppModule) ・・・Angularの起動コンポーネントとして ┃ ┃ 「AppComponent」を定義、 ┃ ┃ 「AppModuleShared」をインポートしたモジュール ┃ ┣ app.module.shared.ts(AppModuleShared) ・・・このアプリケーションが使用する ┃ ┃ コンポーネント ┃ ┃ (/Components配下のコンポーネント)と ┃ ┃ 外部モジュールのインポート宣言をした ┃ ┃ モジュール ┃ ┗ /components ┃ ┣ /app ┃ ┃ ┗ app.component.ts(AppComponent) ・・・SPAのベース/テンプレートとなる ┃ ┃ コンポーネントの実装 ┃ ┗ /その他のディレクトリ ・・・各ページの実装 ┗ /dist ┗ main-server.js ・・・boot.server.tsを起点として依存するファイルを webpackでひとまとめにビルドした結果のファイル
boot.server.ts
boot.server.tsは、サーバサイドレンダリングを実行する際のクライアント側のエントリーポイントです。
Angularが提供するサーバサイドレンダリングを有効化するモジュール(platformDynamicServer)を、ASP.NET Coreが提供するサーバサイドレンダリング用のモジュール(createServerRenderer)でラップしています。
・・・中略 export default createServerRenderer(params => { ・・・(1) const providers = [ { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } }, { provide: APP_BASE_HREF, useValue: params.baseUrl }, { provide: 'BASE_URL', useValue: params.origin + params.baseUrl }, ]; return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => { ・・・(2) const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); const state = moduleRef.injector.get(PlatformState); const zone = moduleRef.injector.get(NgZone); return new Promise<RenderResult>((resolve, reject) => { ・・・(3) zone.onError.subscribe((errorInfo: any) => 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() ・・・(4) }); moduleRef.destroy(); }); }); }); }); });
(1)ではASP.NET Coreより提供されているnpmパッケージである「aspnet-prerendering」のcreateServerRenderer関数を呼び出しています。この関数は、「Angularによるクライアントモジュールの初期化処理結果」を引数にとり、実際にNode.jsを使ってサーバサイドでHTMLを構築する役割を持っています。
「Angularによるクライアントモジュールの初期化処理」を行っているのが(2)以降の処理です。(2)ではまず、Angularが提供するサーバサイドレンダリング用のnpmパッケージ「@angular/platform-server」内のplatformDynamicServer関数を呼び出しています。platformDynamicServer関数の引数にはそのすぐ上で定義しているprovides配列を渡しています。providers配列では、アプリケーションの初期設定情報を定義しています。
platformDynamicServer関数の戻り値を使い、次にbootstrapModule関数を呼び出しています。この関数はルートモジュールであるAppModuleを引数にとりモジュールを起動します(AppModuleについては後ほど説明します)。
モジュールを起動すると、モジュールの参照を非同期で取得できるので、それを使用してレンダリング結果を表すPromiseオブジェクトを生成します(3)。モジュールの参照から取得できるレンダリング結果文字列を(4)のようにオブジェクトのhtmlプロパティに設定してPromiseオブジェクトのresolveとして返すことで、(1)のcreateServerRenderer関数内部でその文字列をDOMに注入することができるようになります。
簡潔化した処理の流れは図1の通りです。
app/app.module.server.ts(ルートモジュール)
Angularアプリケーションのルートモジュールと呼ばれるモジュールです。ルートと呼ばれる通り、Angularを使って作成したコンポーネントは、依存関係をたどると必ずこのルートモジュールに行き着くように実装する必要があります。
import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; import { AppModuleShared } from './app.module.shared'; import { AppComponent } from './components/app/app.component'; @NgModule({ bootstrap: [ AppComponent ], imports: [ ServerModule, ・・・(1) AppModuleShared ・・・(2) ] }) export class AppModule { }
ここでも「ServerModule」という、Angularが提供するサーバサイドレンダリング用のモジュールをインポートします(1)。
また、各画面の実装であるコンポーネントを一括管理しているAppModuleSharedもインポートしています(2)。
app/app.module.shared.ts(AppModuleSharedモジュール)
AppModuleSharedモジュールでは、前述したコンポーネントの一括管理とアプリケーションのルーティング定義を行っています。
・・・中略 @NgModule({ declarations: [ AppComponent, NavMenuComponent, CounterComponent, FetchDataComponent, HomeComponent ], imports: [ CommonModule, HttpModule, FormsModule, RouterModule.forRoot([ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', component: HomeComponent }, { path: 'counter', component: CounterComponent }, { path: 'fetch-data', component: FetchDataComponent }, { path: '**', redirectTo: 'home' } ]) ] }) export class AppModuleShared { }
このアプリケーションで実装するコンポーネントを@NgModuleデコレータのdeclarationで宣言しています。ページを追加実装する際は、ここにコンポーネントを追記していくことになります。
またRouterModuleというAngularが提供するルーターを使用し、URLとコンポーネントの対応づけを行っています。ルーター内の「path」の値がそのままURLとなり、アクセスすると対応するコンポーネントの処理が実行されます。
app/components/app/app.component.ts(ルートコンポーネント)
ルートコンポーネントはアプリケーションの中で最初に呼び出されるコンポーネントです。対応するページ(app.component.html)がベースHTMLとなり、このHTMLのDOMを各ページのものに差し替えていくことでSPAを実現します。
import { Component } from '@angular/core'; @Component({ selector: 'app', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { }
dist/main-server.js
main-server.jsはwebpackによってバンドル・ビルドされたクライアント側のコードです。boot.server.tsを起点としてappディレクトリ配下のコードがひとまとまりになっています。このmain-server.jsを「Views/Home/Index.cshtml」内のasp-prerender-moduleタグヘルパーの値として設定することで、ASP.NET Coreでのサーバサイドレンダリングが有効になります。