前回
前回までで、NestJSのルーティングがどのように実現されているのかを見ました。@Controller
や@Get
といったデコレータによって付与されたメタデータをもとに、RoutesResolver
やRouterExplorer
の働きによってルートパスとハンドラがHTTPサーバーに登録されます。
今回はNestJSのもう一つの重要トピックである依存性注入 (DI)を見ていきます。以前の記事で見た通り、鍵となるのはDependenciesScanner#insertModule
です。
@Injectable
のおさらい
NestJSにおけるDIでは@Injectable
デコレータが重要です。以前の記事で見た通り、このデコレータは以下のようにReflect.defineMetadata
で修飾先のクラスのコンストラクタ関数にメタデータを付与する役割を持っています。
export function Injectable(options?: InjectableOptions): ClassDecorator {
return (target: object) => {
Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
};
}
二つ目のSCOPE_OPTIONS_METADATA
は@Injectable
で指定するinjection scopeを表します。
DIを追う上ではあまり重要ではないので無視して、INJECTABLE_WATERMARK
を追っていくことにします。リポジトリを検索すると、DependenciesScanner
クラスで定義されたisInjectable
メソッドに行き当たります。このメソッドはモジュールクラスのクラスコンストラクタを受け取り、それが@Injectable
修飾されているかを返します。
isInjectable
の利用元であるinsertModule
は以前の記事でも登場しました。
public async insertModule(
moduleDefinition: any,
scope: Type<unknown>[],
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
const moduleToAdd = this.isForwardReference(moduleDefinition)
? moduleDefinition.forwardRef()
: moduleDefinition;
if (
this.isInjectable(moduleToAdd) ||
this.isController(moduleToAdd) ||
this.isExceptionFilter(moduleToAdd)
) {
throw new InvalidClassModuleException(moduleDefinition, scope);
}
return this.container.addModule(moduleToAdd, scope);
}
insertModule
は、モジュールのクラスコンストラクタであるmoduleDefinition
をDIコンテナであるNestConteiner
のインスタンスであるthis.container
に登録し、モジュールクラスをインスタンス化して返します。
insertModule
がどのように使われているかを通じてDependenciesScanner
の全体像を理解しましょう。
DependenciesScanner
DependenciesScanner
は、Nestアプリケーションの依存関係を解決し、DIコンテナにそれを登録する役割を果たします。NestFactory
クラスのinitialze
で利用され、アプリケーションのエントリーポイントになるモジュール(AppModule)を起点にモジュールを再起的にスキャンします。
private async initialize(
module: any, // エントリーポイントになるAppModuleのコンストラクタ
container: NestContainer,
graphInspector: GraphInspector,
config = new ApplicationConfig(),
options: NestApplicationContextOptions = {},
httpServer: HttpServer = null,
) {
// 省略
await dependenciesScanner.scan(module);
await instanceLoader.createInstancesOfDependencies();
dependenciesScanner.applyApplicationProviders();
//省略
ここで、scan
がメインの処理であるAppModuleからの再起的な依存解決を担当し、applyApplicationProviders
はグローバルに扱われるプロバイダーのうちEnhancer(Intercepter, Pipe, Guard, Filter)であるものの登録を担当しています。applyApplicationProviders
はひとまず置いておいて、scan
を見ていきましょう。
dependenciesScanner.scan(module)
実装を見て見ましょう。各処理の責務をコメントで補足しています。
public async scan(
module: Type<any>, // AppModuleのクラスコンストラクタ
options?: { overrides?: ModuleOverride[] },
) {
// InternalCoreModuleFactory.createで生成されるモジュールに対するscanForModules
await this.registerCoreModule(options?.overrides);
// メイン処理 (再起呼び出しされる)
await this.scanForModules({
moduleDefinition: module,
overrides: options?.overrides,
});
// Moduleの解決結果をもとに、各モジュールでImport, Provider, Controller, Exportを反映
await this.scanModulesForDependencies();
// ルートモジュールから出発し、Importsを辿ってモジュール間の「遠さ」を計算して登録
// 初期化順序や依存関係解決の最適化に利用
this.calculateModulesDistance();
// グローバルなEnhancerのスコープ情報をコントローラ, プロバイダーのメタデータに追加する
this.addScopedEnhancersMetadata();
// 各モジュールにグローバルスコープのプロバイダーをバインドし、利用可能にする
this.container.bindGlobalScope();
}
scanForModules
メイン処理であるscanForModules
を見ていきます。理解優先で一部単純化してあります。重要なのは冒頭のthis.insertOrOverrideModule
で、これは先述のinsertModule
を実行し、DIコンテナであるNestContainer
へのクラスの登録とインスタンス化を行います。this.insertOrOverrideModule
以降は、import辿って再起的にこれを実行するためのもろもろです。引数のscope
はAppModuleから現在のモジュールに至るまでの参照解決経路で登場する全てのモジュールのクラスコンストラクタを配列化したものになっています。
public async scanForModules({
moduleDefinition, // 現在着目しているモジュールのクラスコンストラクタ
scope = [],
ctxRegistry = [], // クラスコンストラクタ配列 (処理済みモジュール)
overrides = [],
}: ModulesScanParameters): Promise<Module[]> {
// モジュールを追加、ないし上書きする
// Overrideのことを一旦無視すると、`insertModule`を実行しているだけ
const { moduleRef: moduleInstance, inserted: moduleInserted } =
(await this.insertOrOverrideModule(moduleDefinition, overrides, scope)) ??
{};
// 無駄な処理をスキップするために処理済みのものを管理
ctxRegistry.push(moduleDefinition);
// 着目しているモジュールの@Moduleの引数でimport指定されているモジュール群を取得
const modules = this.reflectMetadata(
MODULE_METADATA.IMPORTS,
moduleDefinition as Type<any>,
);
// 現在のモジュールより下位にあるモジュールのインスタンス全体
let registeredModuleRefs = [];
// importしているモジュールに関してループ
for (const [index, innerModule] of modules.entries()) {
// 処理済みモジュールについてはスキップ
if (ctxRegistry.includes(innerModule)) {
continue;
}
// モジュール探索を再起実行
const moduleRefs = await this.scanForModules({
moduleDefinition: innerModule,
scope: [].concat(scope, moduleDefinition),
ctxRegistry,
overrides,
lazy,
});
registeredModuleRefs = registeredModuleRefs.concat(moduleRefs);
}
if (!moduleInstance) {
return registeredModuleRefs;
}
// グローバルスコープのプロバイダーをバインド
if (moduleInserted) {
this.container.bindGlobalsToImports(moduleInstance);
}
// 現在のモジュールと、それが依存するモジュールに関する再起処理結果を結合し、インスタンス配列を返す
return [moduleInstance].concat(registeredModuleRefs);
}
まとめると、scan
はAppModuleから出発し、@Module
の引数で指定されたimport情報のメタデータに基づいて再起的に参照解決を行い、NestContainer
を更新します。次にこのNestContainer
を見ていきます。
NestContainer
NestJS アプリケーション全体のモジュール構成と依存関係解決の中核を担っています。モジュールとプロバイダーの登録や解決、グローバルスコープの管理、動的モジュールの取り扱い、HTTPアダプターの設定などの重要な機能を実装しています。
DependenciesScanner
から実行されるaddModule
は以下で定義されます。
public async addModule(
metatype: ModuleMetatype,
scope: ModuleScope,
){
// DIコンテナ内でモジュールを一意に識別するトークン (ハッシュ文字列)を生成する。
// scopeやDynamicModuleの設定によって1つのモジュールクラスに対応した複数のモジュールが存在しうる
const { type, dynamicMetadata, token } =
await this.moduleCompiler.compile(metatype);
// 省略
return {
moduleRef: await this.setModule(
{
token,
type,
dynamicMetadata,
},
scope,
),
inserted: true,
};
}
モジュールの登録処理であるsetModule
は以下で、ここでモジュールをインスタンス化し、一意な識別子をキーとして、MapであるModulesContainer
にそのインスタンスを追加しています。
private async setModule(
{ token, dynamicMetadata, type }: ModuleFactory,
scope: ModuleScope,
){
const moduleRef = new Module(type, this);
moduleRef.token = token;
moduleRef.initOnPreview = this.shouldInitOnPreview(type);
// ModulesContainerにトークンをキーとしてモジュールインスタンスを登録
this.modules.set(token, moduleRef);
// type: モジュールのクラスコンストラクタ
const updatedScope = [].concat(scope, type);
// 動的モジュールによって導入された依存関係の処理。
// 動的モジュールのメタデータの保存と、コンテナへのモジュール追加 (addModule)
await this.addDynamicMetadata(token, dynamicMetadata, updatedScope);
if (this.isGlobalModule(type, dynamicMetadata)) {
moduleRef.isGlobal = true;
this.addGlobalModule(moduleRef);
}
return moduleRef;
}
コメントで書いた通り、静的モジュールと動的モジュールは異なる取り扱いを受けます。静的モジュールの依存関係はscanForModules
処理されますが、動的モジュールは上記addDynamicMetadata
内で別途処理されています。
まとめ
今回は、NestJSのもう一つの重要な機能である依存性注入 (DI) について解説しました。特に、DependenciesScanner
クラスがこのプロセスの中核を担っており、scanForModules
メソッドを通じてAppModuleから再帰的に依存関係をスキャンし、DIコンテナであるNestContainer
を更新します。このスキャンと登録のプロセスにより、NestJSはモジュール間の依存関係を把握することができます。
次回は、NestContainer#setModule
で各モジュールのインスタンスが生成されたあと、どのように依存関係が解決され、各種プロバイダーのインスタンス化と依存性注入が行われるかを見ることにします。鍵になるのは、NestFactory
のinitialize
で登場したInstanceLoader#applyApplicationProviders
です。