angular

angular.io FUNDAMENTALS/NgModules メモ

https://angular.io/guide/ngmodule

NgModules

NgModuleは @NgModule デコレータがついたクラスで、どのようにコンパイルして実行するかをAngularに伝えるメタデータオブジェクトを引数に取る。 モジュールのコンポーネント、ディレクティブ、パイプを識別し、外部コンポーネントが使用できるようにする。 DIされるサービスのインスタンスを生成する。

このページでは、NgModuleについて詳しく説明する。

このページではTour of Heroesのサンプルを改善していく過程でNgModuleを説明する。最初から最後までの状態のサンプルが公式ページに用意してある。

Angular modularity

機能やビジネスドメイン、ワークフローなどの単位でモジュールを作る。

モジュールはアプリケーションの開始とともにすぐに読み込まれる (loaded eagerly)。ルーターによって非同期に遅延ロードする(lazy load)こともできる。

シンプルなアプリではルートのモジュール一つでよいが、大きくなるとフィーチャーモジュールに分けたくなる。このページの後半でその過程に触れる。まずはルートモジュールから始める。

The root AppModule

どのAngularアプリケーションにもルートのモジュールがある。

src/app/app.module.ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

BrowserModule はブラウザ上で動作する場合必ず必要で、重要なサービスのプロバイダーを定義していて、また NgIfNgFor のような共通のディレクティブも含んでいる。

angular/packages/platform-browser/src/browser.ts
@NgModule({
  providers: [
    BROWSER_SANITIZATION_PROVIDERS,
    {provide: ErrorHandler, useFactory: errorHandler, deps: []},
    {provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true},
    {provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true},
    {provide: EVENT_MANAGER_PLUGINS, useClass: HammerGesturesPlugin, multi: true},
    {provide: HAMMER_GESTURE_CONFIG, useClass: HammerGestureConfig},
    DomRendererFactory2,
    {provide: RendererFactory2, useExisting: DomRendererFactory2},
    {provide: SharedStylesHost, useExisting: DomSharedStylesHost},
    DomSharedStylesHost,
    Testability,
    EventManager,
    ELEMENT_PROBE_PROVIDERS,
    Meta,
    Title,
  ],
  exports: [CommonModule, ApplicationModule]
})
export class BrowserModule { ... }

declaration にはいまはAppComponentだけで、それをブートストラップしている。アプリケーションが起動すると、index.htmlに <my-app> が表示される。

Bootstrapping in main.ts

main.ts でAppModuleをブートストラップすることで、アプリケーションを起動する。複数のプラットフォームをターゲットとして、何種類かのブートストラップ方法がある。このページでは2つの方法について記述しており、どちらもブラウザをターゲットとしている。

Compile just-in-time (JIT)

最初は動的な方法で、ブラウザ上でAngularコンパイラがアプリケーションをコンパイルし、それからアプリケーションを起動する。

src/main.ts
// The browser platform with a compiler
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

// The app module
import { AppModule } from './app/app.module';

// Compile and launch the module
platformBrowserDynamic().bootstrapModule(AppModule);

A minimal NgModule app

Compile ahead-of-time (AOT)

別の方法は静的で、コードのサイズが小さく起動速度も速いためモバイルやレイテンシーの高いネットワークで特に有効となる。

この静的な方法では、ビルドプロセスの一部として事前にAngularコンパイラが実行され、ファイルにクラスのファクトリーが生成される。これが AppModuleNgFactory となる。

事前にコンパイルされた AppModuleNgFactory をブートストラップするシンタックスは動的な場合とほぼ同じ。

src/main.ts
// The browser platform without a compiler
import { platformBrowser } from '@angular/platform-browser';

// The app module factory produced by the static offline compiler
import { AppModuleNgFactory } from './app/app.module.ngfactory';

// Launch with the app module factory.
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

事前にアプリケーション全体がコンパイルされているので、Angularコンパイラが生成されたコードに含まれないしブラウザ上でコンパイルすることもない。

動的な場合と比べてコードのサイズはずっと小さく、即時に実行することができる。

JITとAOTの両方で AppModule から AppModuleNgFactory を生成する。JITコンパイラはブラウザ上でインメモリにon the flyでファクトリークラスを生成する。AOTコンパイラはファクトリーをファイルに出力し、上記の main.ts でインポートされているのがそれにあたる。

一般的に、AppModuleはどのようにブートストラップされるかを知ったり気にするべきではない。

AppModuleはアプリケーションの成長に従って変化するが、main.tsのブートストラップコードは変化しない。

Declare directives and components

文字をハイライトする HighlightDirective と、それを使う TitleComponent を追加する。 declarations に追加しないといけない。

Service providers

モジュール内のコンポーネントにサービスを供給するのにモジュールはとても良い。

Dependency InjectionのページにAngularの階層的DIシステムとそれをアプリケーションのコンポーネントツリーの異なる階層にprovidersを使ってどのように設定するかが説明されている。

ダミー実装のUserServiceを追加して、それをTitleComponentから使うようにする。

The first contact module

Import supporting modules

src/app/title.component.html
<p *ngIf="user">
  <i>Welcome, {{user}}</i>
<p>

ngIfを使っているが、AppModuleでdeclareしていないにもかかわらずコンパイラでき実行できる。これはインポートしたBrowserModuleが提供している。

より正確には、NgIfはCommonModuleで提供されている。BrowserModuleはCommonModuleをインポートし、エクスポートしているので、BrowserModuleをインポートしていればCommonModuleのディレクティブが使えるようになる。

NgModelやRouterLinkはそれぞれFormsModule、RouterModuleで提供されるので使うときはインポートする。

ここからアプリケーションにContactComponentを追加する。そのコンポーネントはフォームを使うのでFormsModuleをインポートする。

Add the ContactComponent

Angular formsはユーザーのデータ入力を扱うのに優れている。

ContactComponentはcontact editorを提供し、それをAngularフォームのテンプレートドリブンフォームスタイルで実装する。

Angular form styles
Angularでフォームコンポーネントをテンプレートドリブンか、リアクティブスタイルかで書くことができる。テンプレートドリブンの場合は FormsModule を、リアクティブスタイルのばあいは ReactiveFormsModule をインポートする。

(雑にいうと <input [(ngModel)]="userName"> がテンプレートドリブンスタイルで、tsファイル側で FormControl とか FormGroup を使うのがリアクティブスタイル。リアクティブスタイルのほうがアレコレしやすいと思います)

フォームを持つコンポーネントは複雑になりやすい。ContactComponentはContactServiceと、カスタムパイプ(Awesomeパイプ)と、HighlightDirectiveの別バージョンを持っている。

管理しやすくするために、 src/app/contact に機能毎にhtml/ts/cssをわける。

The first contact module

[(ngModel)] をTwo-wayデータバインディングに使っていて、 ngModelNgModel ディレクティブのセレクター。

NgModelはAngularのディレクティブだが、Angularコンパイラは認識できない。

  • AppModuleはNgModelをdeclareしていない
  • NgModelはBrowserModule経由でインポートされていない

Import the FormsModule

FormsModuleをインポートすれば、 [(ngModel)] は動く。

src/app/app.module.ts
imports: [ BrowserModule, FormsModule ],

NgModelFORMS_DIRECTIVES をAppModuleの declarations に加えてはいけない。これらのディレクティブはFormsModuleに属している。コンポーネント、ディレクティブ、パイプは一つのモジュールのみに所属する。他のモジュールに属しているクラスをre-declareしてはいけない

Declare the contact component, directive, and pipe

declareされていないコンポーネント、ディレクティブ、パイプはコンパイルできない。AppModuleのdeclarationsに入れとこう。

src/app/app.module.ts
declarations: [
  AppComponent,
  HighlightDirective,
  TitleComponent,

  AwesomePipe,
  ContactComponent,
  ContactHighlightDirective
],

HighlightDirective という名前のディレクティブが2つある。これに対処するために、JavaScriptのインポートで as を使ってエイリアスする。一旦の解決にはなるものの、別の問題が残る。このあとそれについて説明する。

import {
  HighlightDirective as ContactHighlightDirective
} from './contact/highlight.directive';

Provide the ContactService

ContactComponentのコンストラクタにインジェクトされたContactServiceを使って連絡先を表示している。

サービスはどこかでprovideしなければならず、コンポーネントですることはできるがその場合サービスはそのコンポーネントだけしか使えない。連絡先に関するコンポーネントでサービスを共有することになりそうなので、AppModuleのproviderにContactServiceを書く。

src/app/app.module.ts
providers: [ ContactService, UserService ],

これでアプリケーションのどこからでもContactServiceをインジェクトできる。

Application-scoped providers
ContactServiceはアプリケーションのルートインジェクタにprovidesしたのでアプリケーション全体のスコープになった。
アーキテクチャ的には、ContactServiceは連絡先のビジネスドメインに属している。他のドメインはContactServiceを必要としておらず、インジェクタすべきではない。
Angularがモジュールスコープのインジェクト機能を持っていることを期待するかも知れないが、それはない。NgModuleのインスタンスはコンポーネントとは違ってモジュール自身のインジェクタを持っていないので、モジュールスコープを持てない。
これは意図的なもので、NgModuleはアプリケーションを拡張してモジュールの機能でアプリケーションをリッチにすることを第一に設計されている。
実用上は、サービスのスコープが問題になることはあまりない。連絡先に関係がないコンポーネントがContactServiceを偶然でもインジェクトできない。ContactServiceをインジェクトするためには、クラスの型をインポートする必要がある。連絡先に関連するコンポーネントだけがContactService型をインポートすべきだ。
NgModules FAQページでより詳細に扱う。

Run the app

これでアプリが動くようになった。

The first contact module

Resolve directive conflicts

HighlightDirective と同名のディレクティブを定義したときの問題について触れた。

src/app/highlight.directive.ts
import { Directive, ElementRef } from '@angular/core';

@Directive({ selector: '[highlight]' })
/** Highlight the attached element in gold */
export class HighlightDirective {
  constructor(el: ElementRef) {
    el.nativeElement.style.backgroundColor = 'gold';
    console.log(
      `* AppRoot highlight called for ${el.nativeElement.tagName}`);
  }
}
src/app/contact/highlight.directive.ts
import { Directive, ElementRef } from '@angular/core';

@Directive({ selector: '[highlight], input' })
/** Highlight the attached element or an InputElement in blue */
export class HighlightDirective {
  constructor(el: ElementRef) {
    el.nativeElement.style.backgroundColor = 'powderblue';
    console.log(
      `* Contact highlight called for ${el.nativeElement.tagName}`);
  }
}

どちらのディレクティブもdeclareされているので、両方使うことができる。

同じエレメントに2つのディレクティブが競合した場合、後にdeclareされたものが前の変更を上書きするので勝つ。この場合、contactに定義した方が動作する。

問題は2つの違う暮らしが同じことをしようとしているところにある。
同じディレクティブを複数インポートするのはよい。Angularは重複した方を取り除きひとつだけを登録する。
しかしAngularの視点から、2つの違うクラスは、違うファイルで定義されていて同じ名前のものは重複として扱われない。Angularは2つのディレクティブを保持し、それぞれのディレクティブが同じHTML要素に順番に変更を加える。

少なくともアプリはコンパイルされる。2つの異なるコンポーネントクラスを同じセレクターで定義した場合、コンパイラーはエラーを出す。同じDOMに2つのコンポーネントを挿入することはできない。

コンポーネントとディレクティブの競合をなくすためにはフィーチャーモジュールを作り、もう一方のdeclarationsから一方を隔離する。

Feature modules

まだアプリケーションは大きくないが、既に構造的な問題が起きている。

  • ルートのAppModuleが新しいクラスが追加されるたびに大きくなる
  • ディレクティブが競合している
  • 連絡先に関する機能とそれ以外との明確な境界を失っている。

これらの問題をフィーチャーモジュールで解決する。

フィーチャーモジュールはルートモジュールと同じようにNgModuleのデコレータが付いたクラスで、メタデータも同じ。

ルートモジュールとフィーチャーモジュールは同じ実行コンテキストを共有する。同じインジェクタを共有しているので、一つのモジュールで有効なサービスは他のモジュールでも使える。

これらのモジュールは以下のような違いがある。

  • ルートモジュールはアプリケーションを起動する。ルートモジュールにフィーチャーモジュールをインポートする
  • フィーチャーモジュールは他のモジュールにその実装を公開・隠蔽できる

フィーチャーモジュールはその意図によって区別される。

フィーチャーモジュールはビジネスドメイン、ユーザーワークフロー、機能(forms, http. routing)に集中した一貫性のある機能セットを提供する。

ルートモジュールではすべてのことができる一方でフィーチャーモジュールは特定の関心・目的ごとにアプリケーションを分割するのに役立つ。

フィーチャーモジュールはサービスの供給やコンポーネント、ディレクティブ、パイプを共有してルートモジュールやほかのモジュールと協調する。

次の章では、連絡先の機能をルートモジュールから専用のフィーチャーモジュールに切り出す。

Make Contanct a feature module

連絡先フィーチャーモジュールに関連するものをリファクタするのは簡単だ。

  1. ContactModuleを src/app/contact フォルダに作る
  2. AppModuleからContactModuleに連絡先関係のものを移動する
  3. BrowserModuleのインポートをCommonModuleに変更する
  4. AppModuleでContactModuleをインポートする

Add the ContactModule

src/app/contact/contact.module.ts
import { NgModule }           from '@angular/core';
import { CommonModule }       from '@angular/common';
import { FormsModule }        from '@angular/forms';

import { AwesomePipe }        from './awesome.pipe';

import
       { ContactComponent }   from './contact.component';
import { ContactService }     from './contact.service';
import { HighlightDirective } from './highlight.directive';

@NgModule({
  imports:      [ CommonModule, FormsModule ],
  declarations: [ ContactComponent, HighlightDirective, AwesomePipe ],
  exports:      [ ContactComponent ],
  providers:    [ ContactService ]
})
export class ContactModule { }

HighlightDirective はエクスポートしていないので、AppModuleの方と競合することはない。

Refactor the AppModule

AppModuleで連絡先関係のものを削除する。

他のモジュールでdeclareされたコンポーネント、ディレクティブ、パイプを継承しない。AppModuleがインポートするものとContactModuleには関係がなく、その逆も関係ない。ContactComponentで [(ngModel)] を使うためにはContactModuleでFormsModuleをインポートする必要がある。

Improvements

AppModuleで良くなった点がたくさんある。

  • 連絡先ドメインが大きくなっても変化しない
  • 新しいモジュールを追加したときだけ変更される
  • よりシンプルになった

The revised contact module

Lazy-loading modules with the router

ヒーロースタッフィングエージェンシーアプリケーションはこうなった

  • アプリケーションは3つのフィーチャーモジュールを持つ:連絡先、ヒーロー、危機
  • Routerはこれらのモジュール間を移動するのに役立つ
  • ContactComponentはアプリケーションが起動したときのデフォルトの表示
  • ContactModuleはアプリケーションが起動時に即座(eagerly)に読み込まれる
  • HeroModuleとCrisisModuleは遅延ロードされる
src/app/app.component.ts
template: `
  <app-title [subtitle]="subtitle"></app-title>
  <nav>
    <a routerLink="contact" routerLinkActive="active">Contact</a>
    <a routerLink="crisis"  routerLinkActive="active">Crisis Center</a>
    <a routerLink="heroes"  routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`
src/app/app.module.ts
import { NgModule }           from '@angular/core';
import { BrowserModule }      from '@angular/platform-browser';

/* App Root */
import { AppComponent }       from './app.component.3';
import { HighlightDirective } from './highlight.directive';
import { TitleComponent }     from './title.component';
import { UserService }        from './user.service';

/* Feature Modules */
import { ContactModule }      from './contact/contact.module.3';

/* Routing Module */
import { AppRoutingModule }   from './app-routing.module.3';

@NgModule({
  imports:      [
    BrowserModule,
    ContactModule,
    AppRoutingModule
  ],
  providers:    [ UserService ],
  declarations: [ AppComponent, HighlightDirective, TitleComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

ContactModuleはAppModuleからインポートされているので、ルーティングやコンポーネントはアプリケーションが起動するとマウントされる。

HeroModuleとCrisisModuleはインポートされていない。これらはそのルートにユーザーが遷移したときに非同期にフェッチされマウントされる。

AppRoutingModuleがimportsに追加されており、これはアプリケーションのルーティングの関心を扱うRoutingModuleである。

App routing

Just before adding SharedModule

src/app/app-routing.module.ts
import { NgModule }             from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

export const routes: Routes = [
  { path: '', redirectTo: 'contact', pathMatch: 'full'},
  { path: 'crisis', loadChildren: 'app/crisis/crisis.module#CrisisModule' },
  { path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

ルーティングに関しては Routing & Navigationで扱っているのでここではルーティングとNgModuleについてのみ扱う。

app-routing.module.tsは3つのルートを定義している。

最初のルートは空のURLをcontactのパスにリダイレクトしている。 (http://host.com/ -> http://host.com/contact)

contactのルートはここでは定義されておらず、Contactのフィーチャーモジュールがルーティングモジュールcontact-routing.module.tsを持っている。ルーティングコンポーネントを持つフィーチャーモジュールで自身のルーティングを定義するのは一般的なプラクティスだ。

残りの2つのルートは遅延ローディングのシンタックスを使ってルーターにモジュールの場所を教えている。

遅延ロードされたモジュールの位置は文字列で、型ではない。このアプリケーションでは、モジュールファイルとモジュールクラスを両方指定していて、 # で区切っている。

RouterModule.forRoot

RouterModuleのforRoot静的メソッドは設定を引数にとりモジュールのimportsに加えられ、モジュールのルーティングに関する関心を扱う。

src/app/app-routing.module.ts
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

返されるAppRoutingModuleクラスはRouterModuleのディレクティブと設定されたRouterを生成するDIプロバイダの両方を含むルーティングモジュールとなる。

AppRoutingModuleはルートモジュールのみを対象とする。

フィーチャールーティングモジュールで RouterModule.forRoot を呼んではいけない

AppModuleのimportsにAppRoutingModuleを加えればルーティングが機能する。

src/app/app.module.ts
imports:      [
  BrowserModule,
  ContactModule,
  AppRoutingModule
],

Routing to a feature module

src/app/contactの新しいファイルcontact-routing.module.tsはcontactへのルートを定義しているContactRoutingModuleを提供する。

src/app/contact/contact-routing.module.ts
@NgModule({
  imports: [RouterModule.forChild([
    { path: 'contact', component: ContactComponent }
  ])],
  exports: [RouterModule]
})
export class ContactRoutingModule {}

ここではルーティングのリストをforChildメソッドに渡している。このルーティングのリストは追加のルートを提供することだけの責務を負っていて、フィーチャーモジュールで使われることを意図している。

RouterModule.forChidはフィーチャールーティングモジュールで使う。

forRootとforChildはルートとフィーチャーモジュールにことなるimportの値を提供するための便宜的なメソッド名で、Angularは区別しないが開発者は区別する。 ( ModuleWithProviders を返す意味では同じということ…?)
これと同じようなdeclarationとサービスを共有するモジュールを書く場合はこの規約に従うこと

ルーティングのためにContactComponentを公開する必要はないし、selectorも必要なくなった。

Lazy-loaded routing to a module

遅延ロードされたHeroModuleとCrisisModuleはフィーチャーモジュールとして同じ原則に従う。ContactModuleとは即時ロードされる以外の違いはなさそうに見える。

HeroModuleはCrisisModuleよりも少し複雑なディレクトリ構造になっている。

Just before adding SharedModule

Shared modules

アプリができてきたが、HighlightDirectiveが3種類あったり、フォルダ構造が乱雑になってきている。

共通のコンポーネント、ディレクティブ、パイプを持たせるためにSharedModuleを追加し、これを必要とするモジュールで共有する。

  1. src/app/sharedフォルダを作る
  2. AwesomePipeとHighlightDirectiveをそこに移動する
  3. それぞれのHighlightDirectiveを削除する
  4. SharedModuleクラスを作る
  5. 他のフィーチャーモジュールがSharedModuleをインポートするようにする
src/app/shared/shared.module.ts
import { NgModule }            from '@angular/core';
import { CommonModule }        from '@angular/common';
import { FormsModule }         from '@angular/forms';

import { AwesomePipe }         from './awesome.pipe';
import { HighlightDirective }  from './highlight.directive';

@NgModule({
  imports:      [ CommonModule ],
  declarations: [ AwesomePipe, HighlightDirective ],
  exports:      [ AwesomePipe, HighlightDirective,
                  CommonModule, FormsModule ]
})
export class SharedModule { }
  • 共通のディレクティブを使うのでCommonModuleをインポートしている
  • パイプ、ディレクティブをdeclare, exportしている
  • CommonModuleとFormsModuleを再度exportしている

Re-exporting other modules

アプリケーションを見渡すと、SharedModuleのディレクティブを必要としている多くのコンポーネントが同時にCommonModuleのNgIfやNgForを使い、コンポーネントのプロパティを [(ngModel)] でバインドしていることに気付くだろう。これらのコンポーネントをdeclareしたモジュールはCommonModule、FormsModule、SharedModuleをインポートする必要がある。

SharedModuleがCommonModuleとFormsModuleをre-exportすることで、この繰り返しをなくすことができる。

偶然にも、SharedModuleでdeclareされたコンポーネントは [(ngModel)] のバインディングを使えない。FormsModuleをインポートしていないので。

SharedModuleはFormsModuleをインポートしていなくてもエクスポートすることができる。

Why TitleComponent isn't shared

TitleComponentはAppComponentでしか使わないから

Why UserService isn't shared

多くのコンポーネントは同じサービスインスタンスを共有するが、それはモジュールシステムではなくAngularのDIに依存している。

このサンプルのいくつかのコンポーネントがUserServiceをインジェクトしている。アプリケーション全体で一つだけのUserServiceのインスタンスがあるべきで、プロバイダもただ一つであるべきだ。

UserServiceはアプリケーション全体でシングルトンになる。各モジュールで別のインスタンスを持ちたくないだろうが、SharedModuleがUserServiceをprovideする場合その危険が起きる。

共通のモジュールでアプリケーションのシングルトンを providers に指定してはいけない。共通モジュールをインポートしている遅延ロードモジュールはサービスインスタンスを作る。

The Core Module

この時点で、フォルダのルートはUserServiceとかTitleComponentとかAppComponentでしか使わないやつで煩雑になっている。これらは前述の通りSharedModuleには含めない。

代わりに、これらをCoreModuleに定義する。これをルートモジュールでインポートして、他では使わない。

src/app/core/core.module.ts
import {
  ModuleWithProviders, NgModule,
  Optional, SkipSelf }       from '@angular/core';

import { CommonModule }      from '@angular/common';

import { TitleComponent }    from './title.component';
import { UserService }       from './user.service';
@NgModule({
  imports:      [ CommonModule ],
  declarations: [ TitleComponent ],
  exports:      [ TitleComponent ],
  providers:    [ UserService ]
})
export class CoreModule {
}

使ってないのも(JSの)インポートしているが、あとで使う

TitleComponentをdeclare, exportし、UserServiceをprovideする。TitleComponentが必要とするCommonModuleをインポートする。

CoreModuleはUserServiceを提供している。Angularはアプリのルートのインジェクターにそのproviderを登録し、UserServiceのシングルトンをそれを必要とするどのコンポーネントからも使えるようにする。それがeager/lazyロードされても。

このアプリはサービスのシングルトンとか一度しか使わないコンポーネントについて考えるには小さすぎる。現実のアプリではこれは考慮すべきで、使い捨てのコンポーネントを複数持ち(スピナー、トースト、モーダルとか)これらはAppComponentのテンプレートにのみ表れる。これらは他からは使わないが、ルートフォルダに追いておくにはうざすぎる。
アプリケーションはこの例のUserServiceのように多くのシングルトンのサービスを持つ。これらは確実に一度だけアプリの開始時にルートのインジェクターに登録されなければならない。
多くのコンポーネントがUserServiceみたいなサービスをコンストラクタでインジェクトする一方で(JSのimport文を書かないといけない)、他のコンポーネントやモジュール自身ではそのサービスを定義したり再生成する必要はない。そのprovidersは共有されない。
こういった使い捨てのクラスを集めてそれらの詳細をCoreModuleの中に隠すことを勧める。シンプルになったAppModuleはCoreModuleを全体のオーケストレーターとしてインポートする。

Cleanup

CoreModuleとSharedModuleを追加し、最終的にはこうなる。

The final version

(AppModule, ContactModuleがさっぱりしている様子)

Configure core services with CoreModule.forRoot

アプリケーションにprovidersを追加するモジュールは、そのprovidersを設定する機能も提供できる。

慣例的に、forRoot静的メソッドがサービスのprovidersと設定を同時に提供する。引数にサービスを設定するオブジェクトを取り、 ModuleWithProviders を返す。これは以下のプロパティを持つシンプルなオブジェクト

  • ngModule : CoreModuleクラス
  • providers : 設定されたproviders

ルートのAppModuleはCoreModuleをインポートし、AppModuleのprovidersに追加する。

より正確には、 @NgModule/providers にリストされている項目を追加する前に、インポートされた全てのプロバイダを累積する。これによりAppModuleプロバイダに明示的に追加するものがインポートされたモジュールのプロバイダよりも優先されることを保証する。

UserServiceを設定するCoreModule.forRootメソッドを追加する。

オプショナルにインジェクトされたUserServiceConfigを使ってUserServiceを拡張した。UserServcieConfigが存在する場合、UserServiceはコンフィグからユーザー名を設定する。

src/app/core/user.service.ts
constructor(@Optional() config: UserServiceConfig) {
  if (config) { this._userName = config.userName; }
}
src/app/core/core.module.ts
static forRoot(config: UserServiceConfig): ModuleWithProviders {
  return {
    ngModule: CoreModule,
    providers: [
      {provide: UserServiceConfig, useValue: config }
    ]
  };
}
src/app/app.module.ts
imports: [
  BrowserModule,
  ContactModule,
  CoreModule.forRoot({userName: 'Miss Marple'}),
  AppRoutingModule
],

アプリケーションはデフォルトのSherlock HolmesにかわりMiss Marpleを表示する。

アプリケーションのルートモジュールのAppModuleでのみforRootを実行する。それ以外のモジュール、特に遅延ロードするモジュールで実行すると、実行時エラーが発生するかもしれない。
結果をインポートするのを忘れずに:他のNgModuleには追加しない

Prevent reimport of the CoreModule

AppModuleだけがCoreModuleをインポートすべきで、遅延ロードするモジュールがインポートすると良くないことが起こる

開発者がミスしないことを祈るか、CoreModuleのコンストラクタにエラーを発生させることができる。

src/app/core/core.module.ts
constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
  if (parentModule) {
    throw new Error(
      'CoreModule is already loaded. Import it in the AppModule only');
  }
}

このコンストラクタはAngularにCoreModuleをそれ自身にインジェクトするように指示している。危険な循環参照な感じがする。

Angularが現在のインジェクタでCoreModuleを見つけていれば、インジェクションは循環する。 @SkipSelf デコレータは「自分より上のインジェクタ階層でCoreModuleを探せ」という意味になる。

コンストラクタが意図したとおりにAppModuleで実行されたら、CoreModuleのインスタンスを提供できる上位のインジェクタはいないのでインジェクタはあきらめる。

デフォルトでは、インジェクタは要求されたプロバイダを見つけられなかった場合エラーを投げる。 @Optional デコレータはサービスを見つけられなくても良いという意味になる。インジェクタはnullを返し、parentModuleパラメータはnullになり、コンストラクタは何事もなく終了する。

HeroModuleといった遅延ロードされるモジュールにCoreModuleをインポートした場合は違うことが起きる。

Angularは遅延ロードされたモジュールに対してインジェクタを生成し、ルートインジェクターに対する子になる。 @SkipSelf はAngularにCoreModuleを親のインジェクターから探すようにし、この場合ルートのインジェクタになる。もちろんルートのAppModuleでインポートされたインスタンスを見つける。そしてparentModuleに値が入り、コンストラクタはエラーを吐く。

Conclusion

最終盤をダウンロードするかlive exampleで動かしてみよう

The final version

Frequently asked questions

NgModuleを理解したので、FAQもあるよ