Angular Info

NgModule導入について

どうも、らこです。RC.5のリリースがおそらく来週と迫っていますが、多くのバグ修正と共に新しい機能が追加されます。
NgModuleはこれまでのAngular2で不便だったこと、複雑だったことを一気に解決してくれる新機能です。

RCも大詰めとなったこのタイミングで導入されることに困惑するかもしれませんが、
ぜひとも対応してもらいたいと思います。

はじめに

NgModuleは完全に新しく導入されたAPIであり、既存のAPIへの破壊的変更ではありません。
ただし、従来の方法は非推奨となり、stableリリースの段階では廃止される予定です。
RC.5からは移行期間に入るものと思っていてください。

NgModule

NgModuleの概要についてスライドを作ってあるので、これをベースに解説します。

NgModuleの概要

NgModuleは、ディレクティブやパイプ、サービスなどをひとまとめにしたモジュールを宣言するためのAPIです。
@Componentなどと同じようにデコレータを使って宣言します。

アプリケーションの起動に最低限必要なモジュールは次のようになります。

import {NgModule, ApplicationRef} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {AppComponent} from './app.component';

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

そして、NgModuleで宣言したモジュールを使ってアプリケーションを起動するための、新しいbootstrap関数があります。

import {AppModule} from './app.module';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';

platformBrowserDynamic().bootstrapModule(AppModule);

NgModuleで作ったモジュールを、各プラットフォームのbootstrapModuleメソッドで起動するという流れになります。

NgModuleの中身

それではNgModuleデコレータについてもう少し詳しく見ていきましょう

declarations

このプロパティは、そのモジュールの中で宣言されているディレクティブパイプを登録する場所です。
このディレクティブにはもちろんコンポーネントも含みます。

@NgModule({
     declarations: [
        AppComponent,
        MyComponent,
        MyDirective,
        MyPipe,
        ...SOME_LIBRALIES_DIRECTIVES
    ],
})
class AppModule {}

これまでは、FooDirectiveを宣言したあと、それを使うには@Componentdirectivesプロパティに毎回追加していました。
パイプにおいても同様にpipesプロパティに追加する必要がありました。

NgModuleのdeclarationsに登録されたディレクティブやパイプは、そのモジュール内でならどこでも使えるようになります。
つまり、自作したディレクティブ・コンポーネント・パイプはすべてdeclarationsに登録しておけばよいです。

ROUTER_DIRECTIVESのようなライブラリからインポートしたものも当然declarationsに追加することはできますが、
これに関しては後で説明する方法によって、そもそもROUTER_DIRECTIVESが不要になります。

providers

このプロパティは、そのモジュールのトップレベルのプロバイダを宣言する場所です。
従来のbootstrap関数の第2引数に渡していた配列がそのまま移ってきたと思ってください。

@NgModule({
    providers: [
        MyService,
        SomeLibraryService,
    ],
})
class AppModule {}

完全に旧APIの上位互換となる declarations と違い、@Componentprovidersは非推奨にはなりません。
階層的DIの性質上、任意のコンポーネントでプロバイダを登録できる必要があるからです。
とはいえ、それはライブラリ作者のような複雑な使い方をする人のためのもので、
アプリケーションを普通に作っている中では基本的にすべてNgModuleのprovidersに登録しておけばよいです。

imports

NgModuleの中で一番重要と言えるのがこのimportsです。
importsプロパティを使うことで、自分のモジュールに別のモジュールを取り込むことができます。

例えば、@angular/common@angular/platform-browserから提供されている機能(ディレクティブなど)を取り込むには次のようにします。

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {CommonModule} from '@angular/common';

@NgModule({
    imports: [BrowserModule, CommonModule]
})
class AppModule {}

importsプロパティに登録されたモジュールからは、後述のexportsプロパティで指定されたものと、providersプロパティのプロバイダが取り込まれます。

@angular/routerも専用のモジュールを提供していますが、静的メソッドを使った少し特殊な方法を使います。
インポートと同時にルーティングの設定を渡すことで、動的に生成されたモジュールを取り込んでいます。

import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';

import {APP_ROUTES} from './app.routes';

@NgModule({
    imports: [
        RouterModule.forRoot(APP_ROUTES, {useHash: true})
    ]
})
class AppModule {}

RouterModuleをインポートすれば、自動的にROUTER_DIRECTIVESが取り込まれるので、どこでも<router-outlet>routerLinkを使えるようになります

exports

importsと対をなすのが、このexportsです。
exportsプロパティには、そのモジュールがimportsに設定された時に提供するディレクティブとパイプを指定します。
アプリケーション中ではあまり使うことはなく、基本的にはライブラリ作者用のAPIです。

@NgModule({
    declarations: [AwesomeComponent, AwesomePipe]
    exports: [AwesomeComponent, AwesomePipe],
    providers: [AwesomeService]
})
export class AwesomeModule {
}

bootstrap

bootstrapプロパティには、アプリケーションのエントリポイントになるコンポーネントを指定します。
いままでbootstrap関数に渡していたコンポーネントを指定しておけば大丈夫です。

entryComponents

一番難しいのがこのentryComponentsプロパティです。
このプロパティではオフラインコンパイルを行う時にエントリポイントとなるコンポーネントを指定します。
詳しく説明しようとするとまずオフラインコンパイルの説明からしないといけないので省略しますが、
ここに指定するコンポーネントは、

  • アプリケーションのエントリポイントになるコンポーネント
  • Lazy Loadingのよって実行時にあとから読み込まれるコンポーネント

以上のどちらかに当てはまるコンポーネントです。
ただし、bootstrapプロパティに指定してあるコンポーネントは自動的にentryComponentsにも追加されるので、
後者の、遅延ロードされるコンポーネントだけを指定することになります。

schemas

スライドでは省略しましたが、schemasも面白い機能です。
このプロパティには、アプリケーション中で有効にするスキーマを設定できます。

現在使用可能なスキーマはCUSTOM_ELEMENTS_SCHEMAだけです。
このスキーマを有効にすると、Web標準のCustom ElementsをAngular 2のテンプレート中で使えるようになります。
具体的には、そのモジュール内のディレクティブとコンポーネントには一致せず、要素名に-を含む場合はそれをCustom Elementsとして解釈します。
Custom Elementsであると解釈すれば、その要素は標準のHTML要素と同様にデータバインディングやイベントハンドリングが許可されます。

@Component({
  selector: 'some-component',
  template: `
    <some-custom-element [someUnknownProp]="true"></some-custom-element>
  `,
})
export class SomeComponent {
}

@NgModule({
    schemas: [CUSTOM_ELEMENTS_SCHEMA], 
    declarations: [SomeComponent]
})
export class ModuleUsingCustomElements {
}

RC.4からの移行

NgModuleに関して、移行する必要があるのは次のものです。

  • bootstrap関数の呼び出し
  • @Componentdirectives
  • @Componentpipes

まずはアプリケーションのモジュールをひとつ宣言しましょう。
一般的に必要なモジュールも読み込みます。

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';
import {RouterModule} from '@angular/router';

import {APP_ROUTES} from './app.routes';

@NgModule({
    imports: [BrowserModule, CommonModule, FormsModule, HttpModule, RouterModule.forRoot(APP_ROUTES)]
})
export class AppModule {}

次に、bootstrap関数に渡していたコンポーネント(AppComponentとする)をモジュールに追加します。

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';

import {AppComponent} from './app.component';

@NgModule({
    imports: [BrowserModule, CommonModule, FormsModule, HttpModule],
    declarations: [AppComponent],
    entryComponents: [AppComponent]
})
export class AppModule {}

RC.5の時点では、@Componentdirectivespipesに指定されているディレクティブやパイプは、
そのコンポーネントがdeclarationsに含まれていれば自動的に巻き上げられるようになっています。
なので、とりあえず最初はAppComponentだけをdeclarationsに追加しておけば今までどおりの動作が維持できます。

モジュールが作成できたら、bootstrapする準備をします。
まずはモジュールからコンポーネントを起動する処理をモジュールのコンストラクタに書きます。

import {NgModule, ApplicationRef} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';

import {AppComponent} from './app.component';

@NgModule({
    imports: [BrowserModule, CommonModule, FormsModule, HttpModule],
    declarations: [AppComponent],
    entryComponents: [AppComponent]
})
export class AppModule {

    constructor(appRef: ApplicationRef) {
        appRef.bootstrap(AppComponent);
    }
}

次に、プラットフォームからモジュールを起動する処理を書きます。
これは元々bootstrap関数が呼び出されていた場所を置き換えます。

import {platformBrowserDynamic} from '@angular/browser-platform-dynamic';

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

platformBrowserDynamic().bootstrapModule(AppModule);

これでbootstrapからbootstrapModuleへの移行が完了です。
ここから先は、各コンポーネントに書かれたdirectivespipesdeclarationsに少しずつ回収していけばよいです

テストのNgModule化

現在、テスト用のAPIもモジュール対応が進められています。
こちらは残念ながら破壊的変更が行われます。

  • withProvidersからTestBed.withModuleに変更
  • addProvidersからTestBed.configureTestingModuleに変更
  • TestComponentBuilderが廃止され、TestBed.createComponentに変更

どの変更も、DIを直接使うのではなくテスト用のモジュールを設定してテストするいう流れに変わるものです。

詳細については、公式のチェンジログが待てないというかたは該当のコミットを読むなどするとよいでしょう

まとめ

NgModuleへの置き換えはそれほど大変ではないことがわかったと思います。
そして、今まで複雑だった部分を単純にしてくれる機能であることもわかったでしょうか。
RC.5のリリースは数日中に来ると思われるので、モジュールの波に乗り遅れないように頑張ってください。