経緯
こちらのページで学習中、NG0200: Circular dependency in DI detected for EmployeesService. のエラーに遭遇しました。
// @Injectableを書かないと、このservice内でDIしているAngularFirestoreにアクセスした際に
// NG0200: Circular dependency in DI detected for EmployeesService. が出る
@Injectable()
export abstract class FirestoreService<T> {
}
エラーの解決方法としては上記の通り@Injectable()を付けるだけなのですが、せっかくなので内部で何が起きているかを調べてみたいと思います。
ng-japan OnAir vol.30 "Injectableを再発見!"
まずはこちらで学習します。
(参考)
公式サイト:Angular-Injectable
Angular v6で導入されるTree-Shakable DIの紹介
Angular2のDIを知る
Injectableについて
-
Injectableは、そのclassを提供(provide)可能・注入(inject)可能にするデコレータ - 逆に言えば、その
classが注入されるためには、Injectableがついていなければいけない(constructorでDIされるclassには基本的にInjectableが必要) - v2から登場した
Injectableに、v6でprovidedInのオプションが入った - Tree-Shakable
- v5までは、DIする場合は
NgModuleのproviderに登録しなければならなかった(NgModuleからclassを参照する) - どこからも
Injectionされないclassでも、Providerに登録する時点でNgModuleからの参照が発生し、バンドルに入ってしまうという問題があった - そこでv6で
@Injectableに機能追加を行い、どの単位で参照するかをclass自身が持つようにした(providedInで指定) - これにより逆に
classがNgModuleを参照するようになり、自身がComponentや他のclassから参照されなければ、どこからも参照されない状態になった
@Injectable()のprovidedInプロパティを使用することは、 @NgModule()のproviders 配列を使用するよりも望ましい方法です。 なぜなら、@Injectable()のprovidedInを使用することで、 最適化ツールはアプリケーションで使用されていないサービスをツリーシェイキングし、 バンドルサイズをより小さくできるからです。
(公式サイトより)
-
providedInのoptionとしてはroot、platform、anyがあるが、ほとんどrootしか使わない - 'root' : The application-level injector in most apps.
- angularアプリに対して1個のインスタンスを作る(bootstrap単位でのシングルトン)
ngcコマンドの結果を使用したデバッグについて
-
ngcコマンドでコンパイル後のjsを見ることができる -
@Injectableはɵfacとɵprovを作る -
ɵfacはそのclassをInjectするためのインスタンスをどうやって作るかを定義するFactory関数 -
ɵfacを通ってインスタンスが作られないとDIされない - つまり「
InjectされるためにはInjectableがついていなければいけない」というのは、ɵfacを作ってインスタンスの作り方を定義しなければならないということ -
ɵprovはInjectableをどうprovideするかの設定情報 -
hogeClass.ɵfac is not functionのエラーはInjectableの付け忘れ -
ɵはキーボードで入力できないことから、参照しないでアピールで使う - Angularに詳しくなりたい方は
ngcの結果を読むのはオススメ
コンパイル後のjsを比較してみる
次の3パターンでngcコマンドでコンパイルされたjsを比較します。
| パターン | @Injectable |
provideIn |
|---|---|---|
| 1 | あり | なし |
| 2 | あり | root |
| 3 | なし | - |
パターン1 : @Injectableあり、provideIn指定なし
import { AngularFirestore, QueryFn } from "@angular/fire/compat/firestore";
@Injectable()
export abstract class FirestoreService<T> {
constructor(protected firestore: AngularFirestore;) {}
}
import * as i0 from "@angular/core";
import * as i1 from "@angular/fire/compat/firestore";
export class FirestoreService {
}
FirestoreService.ɵfac = function FirestoreService_Factory(t) { return new (t || FirestoreService)(i0.ɵɵinject(i1.AngularFirestore)); };
FirestoreService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: FirestoreService, factory: FirestoreService.ɵfac });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FirestoreService, [{
type: Injectable
}], function () { return [{ type: i1.AngularFirestore }]; }, null); })();
ɵfacとɵprovが作られていることが分かります。
パターン2 : @Injectableあり、provideIn:'root'
import { AngularFirestore, QueryFn } from "@angular/fire/compat/firestore";
@Injectable({
providedIn: 'root'
})
export abstract class FirestoreService<T> {
constructor(protected firestore: AngularFirestore;) {}
}
import * as i0 from "@angular/core";
import * as i1 from "@angular/fire/compat/firestore";
FirestoreService.ɵfac = function FirestoreService_Factory(t) { return new (t || FirestoreService)(i0.ɵɵinject(i1.AngularFirestore)); };
FirestoreService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: FirestoreService, factory: FirestoreService.ɵfac, providedIn: 'root' });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FirestoreService, [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], function () { return [{ type: i1.AngularFirestore }]; }, null); })();
ɵɵdefineInjectable の引数となっているオブジェクトのプロパティにprovidedIn: 'root'が追加されています。
パターン3 : @Injectableなし
import { AngularFirestore, QueryFn } from "@angular/fire/compat/firestore";
// @Injectable() <-- なし
export abstract class FirestoreService<T> {
constructor(protected firestore: AngularFirestore;) {}
}
// import * as i0 from "@angular/core"; <-- なくなった
// import * as i1 from "@angular/fire/compat/firestore"; <-- なくなった
export class FirestoreService {
}
// ↓ なくなった
// FirestoreService.ɵfac = function FirestoreService_Factory(t) { return new (t || FirestoreService)(i0.ɵɵinject(i1.AngularFirestore)); };
// FirestoreService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: FirestoreService, factory: FirestoreService.ɵfac });
// (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FirestoreService, [{
// type: Injectable
// }], function () { return [{ type: i1.AngularFirestore }]; }, null); })();
ɵfacとɵprovが作られていません。
また、factory関数内のi0.ɵɵinject(i1.AngularFirestore)が消え、そもそもAngularFirestoreがimportされていません。
firestore.service.tsに@Injectable()をつけないとNG0200: Circular dependency in DI detected for EmployeesService. が出る問題は、これが原因と思われます。
が、なんでCircular dependencyになるのかは分かりません・・・
-
/*@__PURE__*/について
@Injectable()(ProvidedInの指定なし)は何を意味するのか?
providedIn
Determines which injectors will provide the injectable.providedIn?: Type | 'root' | 'platform' | 'any' | null
- 'null' : Equivalent to undefined. The injectable is not provided in any scope automatically and must be added to a providers array of an @NgModule, @Component or @Directive.
(公式サイトより)
-
providedInは、どのinjectorがinjectableをprovideするか決めるもの -
@Injectable()と@Injectable({providedIn:null})同じで、どのスコープにも自動的にはprovideされない
ちなみに、injectorは3種類あります
| Injector | configuredBy | providedIn option | 備考 |
|---|---|---|---|
| NullInjector |
@Optionalを使わない限りエラーを投げる |
||
| ModuleInjector | PlatformModule | platform | |
| root ModuleInjector | AppModule | root |
@Injectable()
export abstract class FirestoreService<T> {
}
@Injectable()をつけただけではprovideされないので、FirestoreServiceをどこかのclassでInjectするとエラーが出ると予想されます。
import { FirestoreService } from './firestore.service';
export class EmployeesService {
constructor(private fs: FirestoreService<Employee>) { }
}
実行するとやはりエラーが出ました。
NullInjectorError: No provider for FirestoreService!
他のclassで使いたいclassには@Injectable()を付けるだけでは十分でなく、providedInでInjectorを決めてやる必要があるようです。