在某些應用場景下,可能會需要去遍歷封裝於 模組(Module) 內的元件,比如:找出帶有特定 裝飾器(Decorator) 的元件,甚至是元件底下的方法,來預先處理一些事情,最典型的案例就是 EventEmitterModule
,當某個事件觸發時,會呼叫帶有特定裝飾器的方法。
NOTE:關於 EventEmitterModule
可以參考官方文件的說明。
下方是官方 EventEmitterModule
的範例,透過 EventEmitter2
發送 order.created
事件時,會呼叫帶有 @OnEvent
裝飾器且值為 order.created
的方法:
1 2 3 4 5 6 7
| this.eventEmitter.emit( 'order.created', new OrderCreatedEvent({ orderId: 1, payload: {}, }), );
|
1 2 3 4 5 6 7 8 9
| @Injectable() export class OrderListener { @OnEvent('order.created') handleOrderCreatedEvent(payload: OrderCreatedEvent) { } }
|
那麼 EventEmitterModule
是如何做到這件事情的呢?它是透過一個叫 DiscoveryModule
的模組來找出所有元件底下含有 @OnEvent
裝飾器的方法,並根據帶入的值,來決定該方法在哪個事件下會被觸發。
NOTE:DiscoveryModule
並沒有收錄在 NestJS 官方文件中。
深入 Discovery Module
NOTE:以下範例採用 NestJS 10 來撰寫。
DiscoveryModule
是一個 NestJS 內建的模組,無須安裝套件,使用方式如下:
1 2 3 4 5 6 7 8 9 10
| import { Module } from '@nestjs/common'; import { DiscoveryModule } from '@nestjs/core';
@Module({ imports: [DiscoveryModule], }) export class AppModule {}
|
引入模組後,可以透過 DiscoveryService
來取得封裝於模組底下的 Controller 或 Provider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { Module } from '@nestjs/common'; import { DiscoveryModule, DiscoveryService } from '@nestjs/core';
@Module({ imports: [DiscoveryModule], }) export class AppModule implements OnModuleInit { constructor( private readonly discoveryService: DiscoveryService ) {}
onModuleInit() { const controllers = this.discoveryService.getControllers(); const providers = this.discoveryService.getProviders(); } }
|
這裡需特別注意,取得的 不是 Controller、Provider 本身,而是一個型別為 InstanceWrapper
的 Wrapper,若要拿到它們的本身的 實例(Instance),只需要透過 instance
屬性即可取得,如下所示:
1 2
| const instances = this.discoveryService.getControllers().map(({ instance }) => instance);
|
限縮遍歷範圍
如果想要限制遍歷的模組範圍,getControllers
跟 getProviders
有提供相關參數,透過指定 include
來決定要遍歷哪些模組:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { Module, OnModuleInit } from '@nestjs/common'; import { DiscoveryModule, DiscoveryService } from '@nestjs/core';
@Module({ imports: [TodoModule, DiscoveryModule], }) export class AppModule implements OnModuleInit { constructor( private readonly discoveryService: DiscoveryService ) {}
onModuleInit() { const providers = this.discoveryService.getProviders({ include: [TodoModule], }); const controllers = this.discoveryService.getControllers({ include: [TodoModule], }); } }
|
過濾別名 Provider 的技巧
由於 getProviders
會拿到所有 Provider,所有裡面會含有 Alias Provider,在某些情境下有可能會導致相同的東西被處理一次以上,所以在預處理前,要先進行過濾:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { Module, OnModuleInit } from '@nestjs/common'; import { DiscoveryModule, DiscoveryService } from '@nestjs/core';
@Module({ imports: [DiscoveryModule], }) export class AppModule implements OnModuleInit { constructor( private readonly discoveryService: DiscoveryService, private readonly metadataScanner: MetadataScanner ) {}
onModuleInit() { const providers = this.discoveryService.getProviders(); const controllers = this.discoveryService.getControllers(); [...providers, ...controllers] .filter((wrapper) => wrapper.instance && !wrapper.isAlias) .forEach((wrapper) => { }); } }
|
現在知道要如何透過 DiscoveryModule
遍歷所有元件了,那有什麼方法可以取得元件底下所有的方法呢?NestJS 有提供一個叫 MetadataScanner
的 Provider,讓我們可以去掃描元件下的所有方法,使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { Module, OnModuleInit } from '@nestjs/common'; import { MetadataScanner } from '@nestjs/core';
@Module({ }) export class AppModule implements OnModuleInit { constructor( private readonly metadataScanner: MetadataScanner ) {}
onModuleInit() { const instance = new Component(); const methodNames = this.metadataScanner.getAllMethodNames(instance); } }
|
那麼加上 DiscoveryModule
,就可以遍歷所有元件底下的方法名稱了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { Module, OnModuleInit } from '@nestjs/common'; import { DiscoveryModule, DiscoveryService, MetadataScanner } from '@nestjs/core';
@Module({ imports: [DiscoveryModule], }) export class AppModule implements OnModuleInit { constructor( private readonly discoveryService: DiscoveryService, private readonly metadataScanner: MetadataScanner ) {}
onModuleInit() { const providers = this.discoveryService.getProviders(); const controllers = this.discoveryService.getControllers();
[...providers, ...controllers] .filter((wrapper) => wrapper.instance && !wrapper.isAlias) .forEach((wrapper) => { const { instance } = wrapper; const methodNames = this.metadataScanner.getAllMethodNames(instance); }); } }
|
搭配 Reflector 打出連續技
假設現在需要抓取所有元件下帶有 HelloWorld
裝飾器的方法,可以運用 DiscoveryModule
先遍歷所有的元件,再透過 MetadataScanner
掃出每個元件下的方法名稱,最後再使用 Reflector
篩選出最終結果。
假設現在有一個 @HelloWorld
裝飾器:
1 2 3 4 5
| import { SetMetadata } from '@nestjs/common';
export const HELLO_WORLD_KEY = 'custom:hello-word';
export const HelloWorld = () => SetMetadata(HELLO_WORLD_KEY, 'Hello World');
|
並且只在 TodoModule
底下的 TodoController
中使用:
1 2 3 4 5 6 7 8 9 10 11
| import { Controller, Get } from '@nestjs/common';
@Controller('todos') export class TodoController { @HelloWorld() @Get() getTodos() { return []; } }
|
這時可以運用 Reflector
的 get
方法,來判斷元件底下的方法是否有使用 @HelloWorld
裝飾器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import { Module, OnModuleInit } from '@nestjs/common'; import { DiscoveryModule, DiscoveryService, MetadataScanner, Reflector, } from '@nestjs/core';
@Module({ imports: [TodoModule, DiscoveryModule], }) export class AppModule implements OnModuleInit { constructor( private readonly discoveryService: DiscoveryService, private readonly metadataScanner: MetadataScanner, private readonly reflector: Reflector, ) {}
onModuleInit() { const providers = this.discoveryService.getProviders(); const controllers = this.discoveryService.getControllers(); [...providers, ...controllers] .filter((wrapper) => wrapper.instance && !wrapper.isAlias) .forEach((wrapper) => { const { instance } = wrapper; const methodNames = this.metadataScanner.getAllMethodNames(instance); methodNames .filter( (methodName) => this.reflector.get<string>( HELLO_WORLD_KEY, instance[methodName], ) === 'Hello World', ) .forEach((methodName) => { console.log(methodName); }); }); } }
|
結論
DiscoveryModule
是一個蠻好用的內建模組,尤其是針對一些事件驅動的情境特別適合,比如說:使用第三方的 SDK,它收到某個事件時可以呼叫我們帶有特定裝飾器的方法等。
參考資料