Angular4 で Service を DI する方法

Angular4 における DI (Dependency Injection・依存性注入) に関連して、Component から Service を使うときに Service クラスをどう Angular モジュールとして突っ込んでやればいいかなというのを調べてみた。

今回のサンプルコードでは、MyPageListService という一つの Service を、色んな形で DI するようにしている。イメージとしては、同階層にある MyPageListComponent でしか使わない Service、といった位置付け。

それでも色々な形で Service を登録しておき Component から DI できるということを示すため、MyPageListService をルートの NgModule に登録してみたり、中間の NgModule に登録してみたりアレコレしている。この例のとおりに構成すると、ディレクトリ構成などが不自然なことになるので注意してほしい。

共通的な Service である場合

広く使う Service は app.module.ts@NgModuleproviders に突っ込むと動くようになる。

その Service を使う Component では constructor の引数で当該 Service を受け取り、Component 自身のフィールド変数に持たせて使用する。その Service を扱える Component が増えるため、本来は共通的な処理のモノのみ、ルートの NgModule に追加すると良い。

また、今回は紹介していないが、アプリ全体で共通的に使用するモジュール群は shared というディレクトリを作り、SharedModule を作ってそこに providers として登録するのが Angular 公式で推奨されているスタイルガイドとなっている。つまり app.module.ts では基本的に providers に Service を登録するようなことはほとんどなく、全ては imports している配下の NgModule が行う、という作りだ。

/* ./src/app/app.module.ts */
import { NgModule }          from '@angular/core';
import { BrowserModule }     from '@angular/platform-browser';
import { AppRoutingModule }  from './app-routing.module';      // ルーティング
import { AppComponent }      from './app.component';           // ルートの Component
import { MyPageModule }      from './my-page/my-page.module';  // my-page 配下をまとめる NgModule
// DI したい Service : ここではサンプルのため、共通的に読み込むべき Service ではないものの、ルートで登録してみている
import { MyPageListService } from './my-page/my-page-list/my-page-list.service';

@NgModule({
  imports     : [BrowserModule, AppRoutingModule, MyPageModule],
  declarations: [AppComponent],
  providers   : [MyPageListService],  // アプリのルートの NgModule で Providers に指定する
  bootstrap   : [AppComponent]
})
export class AppModule { }
/* ./src/app/my-page/my-page.module.ts */
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyPageRoutingModule } from './my-page-routing.module';
import { MyPageListComponent } from './my-page-list/my-page-list.component';
import { MyPageEditComponent } from './my-page-edit/my-page-edit.component';

// 中間の my-page NgModule には MyPageListService は登場しない
// ココに providers: [MyPageListService] と書いても問題はない (Service は 重複して providers に指定しても問題ない様子)
// (Component は 複数の NgModule の declarations に書くとエラーになる)
@NgModule({
  imports     : [CommonModule, MyPageRoutingModule],
  declarations: [MyPageListComponent, MyPageEditComponent]
})
export class MyPageModule { }
/* ./src/app/my-page/my-page-list/my-page-list.component.ts */
import { Component, OnInit } from '@angular/core';
// 型を知るための import であって、コレによって new したりするワケではない
import { MyPageListService } from './my-page-list.service';

// Component の providers は指定していない
@Component({
  selector   : 'app-my-page-list',
  templateUrl: './my-page-list.component.html',
  styleUrls  : ['./my-page-list.component.scss']
})
export class MyPageListComponent implements OnInit {
  // 取得した Service を控えておくフィールド変数
  myPageListService: MyPageListService;
  
  // 引数でいきなり MyPageListService を受け取っている
  constructor(myPageListService: MyPageListService) {
    this.myPageListService = MyPageListService;
  }
}

Service の DI の仕方

今回の例は全てフィールド変数を定義するような書き方にしている。これは ES2015 からの移行時に分かりやすい。しかし、TypeScript だと constructor の引数に private などのアクセス修飾子を付与することで、その引数をフィールド変数として持てるようになる。つまりこういうことだ。

/* ./src/app/my-page/my-page-list/my-page-list.component.ts */
@Component({ /* 省略 */ })
export class MyPageListComponent implements OnInit {
  // アクセス修飾子 private を付けると、フィールド変数の定義なしに this.myPageListService が作れるようになる
  constructor(private myPageListService: MyPageListService) {
    // いきなり this.myPageListService のメソッドを叩く例
    this.myPageListService.hogeMethod();
  }
}

この方が TypeScript らしい書き方になるのかもしれないが、ES2015 から移行してきた感覚では少し違和感。そして ES2015 に戻りづらくなりそう…。

Component の場合は異なる NgModule で重複しないこと

途中で触れたが、Component の場合は複数の NgModule の declarations に書いてしまうと、以下のようなエラーが出る (ng serve 時にコンソールで確認できる)。

Error: Type MyPageListComponent is part of the declarations of 2 modules: MyPageModule and AppModule!
       Please consider moving MyPageListComponent to a higher module that imports MyPageModule and AppModule.
       You can also create a new NgModule that exports and includes MyPageListComponent then import that NgModule in MyPageModule and AppModule.

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

Service を異なる NgModule の providers に重複して書くのは良いみたい。

その機能配下で共通的に使用する Service の場合

共通的な Service だからといって、何でもかんでも app.module.ts に突っ込んでしまうと、app.module.ts が肥大化してしまい、見通しが悪くなる。Angular には NgModule というモジュール分割の機能があるので、コレを使って、アプリ内の機能ごと ≒ ディレクトリごとに NgModule を作ってやろう。

Angular 公式のスタイルガイドなどを見ていると、この機能別に作る NgModule のことを「Feature Modules」と呼んでいる。やはり「機能別」のようだ。

アプリのルートで存在を知らなくて良い Service は、なるべくその Service を使用する範囲を狭めるよう、当該 Service から一番近い NgModule に含ませてやると良いだろう。

もし、MyPageListService./src/my-page/ 配下の色々な Component で使うようなものだとしたら、MyPageModuleproviders に登録しておくイメージだ。

/* ./src/app/app.module.ts */
import { NgModule }          from '@angular/core';
import { BrowserModule }     from '@angular/platform-browser';
import { AppRoutingModule }  from './app-routing.module';
import { AppComponent }      from './app.component';
import { MyPageModule }      from './my-page/my-page.module';  // my-page 配下をまとめる NgModule : コレに MyPageListService を追加する

// ルートでは providers に MyPageListService を指定しない
@NgModule({
  imports     : [BrowserModule, AppRoutingModule, MyPageModule],  // ← MyPageModule はココでルートの NgModule に含ませている
  declarations: [AppComponent],
  bootstrap   : [AppComponent]
})
export class AppModule { }
/* ./src/app/my-page/my-page.module.ts */
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyPageRoutingModule } from './my-page-routing.module';
import { MyPageListComponent } from './my-page-list/my-page-list.component';
import { MyPageEditComponent } from './my-page-edit/my-page-edit.component';
// DI したい Service
import { MyPageListService   } from './my-page-list/my-page-list.service';

// 中間の NgModule に Service を登録する
// コレでこの場合、MyPageListComponent・MyPageEditComponent から MyPageListService を使える状態になる
@NgModule({
  imports     : [CommonModule, MyPageRoutingModule],
  declarations: [MyPageListComponent, MyPageEditComponent]
  providers   : [MyPageListService]
})
export class MyPageModule { }
/* ./src/app/my-page/my-page-list/my-page-list.component.ts */
import { Component, OnInit } from '@angular/core';
import { MyPageListService } from './my-page-list.service';

// Component の作りは変わらず。
@Component({
  selector   : 'app-my-page-list',
  templateUrl: './my-page-list.component.html',
  styleUrls  : ['./my-page-list.component.scss']
})
export class MyPageListComponent implements OnInit {
  myPageListService: MyPageListService;
  
  constructor(myPageListService: MyPageListService) {
    this.myPageListService = MyPageListService;
  }
}

当該 Component でしか使わない Service の場合

その Component 自身でしか使わない Service であれば、@Componentproviders を書くことができる。こうしておけば、Service の利用範囲を狭めることができる。

/* ./src/app/app.module.ts */
import { NgModule }          from '@angular/core';
import { BrowserModule }     from '@angular/platform-browser';
import { AppRoutingModule }  from './app-routing.module';
import { AppComponent }      from './app.component';
import { MyPageModule }      from './my-page/my-page.module';

// ルートでは providers に MyPageListService を指定しない
@NgModule({
  imports     : [BrowserModule, AppRoutingModule, MyPageModule],
  declarations: [AppComponent],
  bootstrap   : [AppComponent]
})
export class AppModule { }
/* ./src/app/my-page/my-page.module.ts */
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyPageRoutingModule } from './my-page-routing.module';
import { MyPageListComponent } from './my-page-list/my-page-list.component';
import { MyPageEditComponent } from './my-page-edit/my-page-edit.component';

// 中間の NgModule でも providers に MyPageListService を指定しない
@NgModule({
  imports     : [CommonModule, MyPageRoutingModule],
  declarations: [MyPageListComponent, MyPageEditComponent]
})
export class MyPageModule { }
/* ./src/app/my-page/my-page-list/my-page-list.component.ts */
import { Component, OnInit } from '@angular/core';
import { MyPageListService } from './my-page-list.service';

// Component の providers で指定する
@Component({
  selector   : 'app-my-page-list',
  templateUrl: './my-page-list.component.html',
  styleUrls  : ['./my-page-list.component.scss'],
  providers  : [MyPageListService]  // ← ココで Service を指定する
})
export class MyPageListComponent implements OnInit {
  myPageListService: MyPageListService;
  
  // 以降の扱い方は同じ。
  constructor(myPageListService: MyPageListService) {
    this.myPageListService = MyPageListService;
  }
}
/* ./src/app/my-page/my-page-edit/my-page-edit.component.ts */
import { Component, OnInit } from '@angular/core';
// ページごとのコンポーネントのイメージなので、階層を遡って取得している
import { MyPageListService } from '../my-page-list/my-page-list.service';

// 今回の例の作り上は強引になるが
// もし別のコンポーネント MyPageEditComponent が providers に指定しても問題ない
@Component({
  selector   : 'app-my-page-edit',
  templateUrl: './my-page-edit.component.html',
  styleUrls  : ['./my-page-edit.component.scss'],
  providers  : [MyPageListService]  // ← ココで Service を指定できる
})
export class MyPageEditComponent implements OnInit {
  myPageListService: MyPageEditService;
  
  // 以降の扱い方は同じ。
  constructor(myPageEditService: MyPageEditService) {
    this.myPageEditService = MyPageEditService;
  }
}

総括

Component の場合は異なる NgModule の declarations に重複して書けないので、Service もそうなのかと思ったら、Service は重複して providers に書いても大丈夫みたい。

あんまりちゃんと検証していないので副作用があるかもしれないし、普通に機能ごと・ページごとにコンポーネントやサービスを作っていれば、重複して providers に書きたくなる Service はまず発生しないはず。

だいたいこんな感じになるのではないだろうか。

shared というディレクトリ構成は Angular 公式のスタイルガイドにも登場している。ルートの app に関連するモジュールは core、異なる機能間で共通的に使うモジュールは shared というディレクトリを作るようだ。

その他参考