Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

ASP.NET Core 2.0でSPAテンプレートを使おう【後編】

進化した「ASP.NET Core 2.0」新しいWeb開発手法を学ぶ 第4回

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2018/01/23 14:00

 前回はASP.NET CoreとSPA(Single Page Application)フレームワークを組み合わせたアプリケーションでSSR(サーバサイドレンダリング)を行うための仕組みについてサーバ側の構成を中心に説明しました。今回はクライアント側の構成について説明し、実際にページを追加する手順についても紹介します。

目次

対象読者

  • 新しい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)でラップしています。

リスト1 クライアント側のエントリーポイント(ClientApp/boot.server.ts)
・・・中略
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の通りです。

図1 boot.server.tsの処理の流れ
図1 boot.server.tsの処理の流れ

app/app.module.server.ts(ルートモジュール)

 Angularアプリケーションのルートモジュールと呼ばれるモジュールです。ルートと呼ばれる通り、Angularを使って作成したコンポーネントは、依存関係をたどると必ずこのルートモジュールに行き着くように実装する必要があります。

リスト2 ルートモジュール(ClientApp/app/app.module.server.ts)
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モジュールでは、前述したコンポーネントの一括管理とアプリケーションのルーティング定義を行っています。

リスト3 AppModuleSharedモジュール(ClientApp/app/app.module.shared.ts)
・・・中略
@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を実現します。

リスト4 ルートコンポーネント(ClientApp/app/components/app/app.component.ts)
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でのサーバサイドレンダリングが有効になります。


  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • WINGSプロジェクト 秋葉 龍一(アキバ リュウイチ)

    <WINGSプロジェクトについて> 有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂...

  • 山田 祥寛(ヤマダ ヨシヒロ)

    静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for ASP/ASP.NET。執筆コミュニティ「WINGSプロジェクト」代表。 主な著書に「入門シリーズ(サーバサイドAjax/XMLD...

バックナンバー

連載:進化した「ASP.NET Core 2.0」新しいWeb開発手法を学ぶ
All contents copyright © 2005-2018 Shoeisha Co., Ltd. All rights reserved. ver.1.5