経緯
こちらのページで学習中、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
を決めてやる必要があるようです。