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
の @NgModule
の providers
に突っ込むと動くようになる。
その 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 で使うようなものだとしたら、MyPageModule
の providers
に登録しておくイメージだ。
/* ./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 であれば、@Component
に providers
を書くことができる。こうしておけば、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 はまず発生しないはず。
- アプリ全体で共通的な Service なら
./src/app/shared/hoge.service.ts
といった階層で Service を作っておき./src/app/shared/shared.module.ts
のproviders
に登録する./src/app/app.module.ts
はshared.module.ts
をimports
しておくのみ
- その機能内で共通的な Service なら
./src/app/hoge/shared/hoge.service.ts
といった階層で Service を作っておき./src/app/hoge/hoge.module.ts
のproviders
に登録する./src/app/app.module.ts
はhoge.module.ts
をimports
しておくのみ
- そのコンポーネントのみで使用する Service なら
./src/app/hoge/fuga/fuga.service.ts
といった階層で Service を作っておき./src/app/hoge/fuga/fuga.component.ts
のproviders
に登録して使う./src/app/app.module.ts
はfuga.component.ts
をdeclarations
に登録するか、配下の NgModule をimports
することで対応する
だいたいこんな感じになるのではないだろうか。
shared
というディレクトリ構成は Angular 公式のスタイルガイドにも登場している。ルートの app
に関連するモジュールは core
、異なる機能間で共通的に使うモジュールは shared
というディレクトリを作るようだ。
- 参考 : Angular Docs
- 参考 : Angular Docs
- 参考 : Angular2 DOC GUIDEを翻訳するSTYLE GUIDE - Qiita
- 参考 : Angular2 DOC GUIDEを翻訳するSTYLE GUIDE - Qiita