很久没写公众号了,比较忙。。。
今天分享一篇 NestJS Dynamic Module 实战示例,以加深自己对它的理解。
1背景
发现项目中使用 NestJS Logger
时都是这样用:
@Injectable()class HelloService { private readonly logger = new Logger('HelloService');}@Injectable()class WorldService { private readonly logger = new Logger('WorldService');}
这种重复于多处的、手动的初始化代码让人感觉不舒服。于是想,有更方便的管理办法吗?比如,让 NestJS 管理这些 Logger
的初始化和注入?
2想要的效果
我们期望达到这种效果:
@Injectable()class HelloService { constructor(@Logger('HelloService') private readonly logger: LoggerService) {}}
即,当需要使用 logger 时,通过 @Logger
指令自动注入,并且,项目中不会出现 new Logger()
这类操作。
(还有一小点要注意的是,这里我们使用的是 LoggerService
,而非 Logger
,前者是接口,后者实现了前者,我们倾向于 面向接口编程。)
那么如何实现呢?
3如何实现自定义注入指令
通常我们使用 @Inject
来管理注入。但如果使用 @Inject('HelloService')
,名为 'HelloService'
的 logger
对象其实并不存在,而且它和 HelloService
这个 class 还会命名冲突。
鉴于此,我们打算基于 @Inject
指令,开发一个新的指令——就叫它 @Logger
吧!
@Logger
的代码如下:
import { Inject } from '@nestjs/common';export const prefixesForLoggers: string[] = [];export function Logger(prefix = '') { if (!prefixesForLoggers.includes(prefix)) { prefixesForLoggers.push(prefix); } return Inject(`${prefix}Logger`);}
可以看到,@Logger
要做的是,当使用 @Logger('HelloService')
时,就是在使用 @Inject('HelloServiceLogger')
。
你可能会疑惑,名为 'HelloServiceLogger'
provider 是在哪里注册的?别急,上面的 prefixesForLoggers
数组就是为了动态地注册这些 logger providers 做准备的。
4动态注册 logger providers
首先,由于每个使用到 logger 的组件都是用各自的 logger 实例(因为 logger 的名称(token)各不相同),所以我们需要先定义一个 Scope 为 TRANSIENT
的 logger provider:
文件:logger.provider.ts
import { prefixesForLoggers } from './logger.decorator';import { Logger, Provider, Scope } from '@nestjs/common';export const baseLoggerProvider = { provide: Logger, useClass: Logger, scope: Scope.TRANSIENT,};
它的作用有2个,一是所有的 logger 实例都由它提供;二是每个组件拿到的都是新的 logger 实例。
接下来就是根据 @Logger
的调用信息,动态配置 baseLoggerProvider
生成的 logger 实例:
// 根据 logger 的 prefix 参数,动态生成 logger providerfunction createLoggerProvider(prefix: string): Provider<Logger> { const provider = { provide: `${prefix}Logger`, useFactory: (logger: Logger) => { if (prefix) { logger.setContext(prefix); } return logger; }, inject: [Logger], scope: Scope.DEFAULT, }; return provider;}export function createLoggerProviders(): Array<Provider<Logger>> { return prefixesForLoggers.map((prefix) => createLoggerProvider(prefix));}
最后,我们用一个 LoggerModule
来集成上面的封装:
import { DynamicModule } from '@nestjs/common';import { baseLoggerProvider, createLoggerProviders } from './logger.provider';export class LoggerModule { static forRoot(): DynamicModule { const prefixedLoggerProviders = createLoggerProviders(); return { module: LoggerModule, providers: [baseLoggerProvider, ...prefixedLoggerProviders], exports: [baseLoggerProvider, ...prefixedLoggerProviders], }; }}
注意,这里我们使用的是 DynamicModule
。因为按照官方的说法[1],当模块间 被消费的一方(LoggerModule
)的运行依赖于消费的一方(某个 import 了 LoggerModule
的模块)时,需要用 Dynamic modules 进行编写。换言之,当消费的一方需要向被消费的一方传递参数以帮助被消费一方完成初始化时,使用 DynamicModule
方式来编写。
(应用到本文,则是 LoggerModule
的运行需要收集 @Logger
调用的参数,以生成动态的 logger provider 定义,而这些参数很明显是消费方传递过来的,故而使用 DynamicModule
)。
5使用
一个简单的使用示例如下:
HelloModule.ts
:
import { Module } from '@nestjs/common';import { HelloService } from './hello.service';import { LoggerModule } from '../logger/logger.module';
HelloService.ts
,我们注入2个 logger ,以观察是否符合预期:
import { Injectable, LoggerService } from '@nestjs/common';import { Logger } from '../logger/logger.decorator';export class HelloService {
constructor(
@Logger('Hello1Service') private readonly logger1: LoggerService,
@Logger('Hello2Service') private readonly logger2: LoggerService,
) {}
}
调试代码,可以看到 2个logger 都被正确地创建了:

6总结
休息,不总结了 🤣 。
参考资料
Dynamic modules: https://docs.nestjs.com/fundamentals/dynamic-modules
[2]Advanced NestJS: How to build completely dynamic NestJS modules: https://dev.to/nestjs/advanced-nestjs-how-to-build-completely-dynamic-nestjs-modules-1370
[3]Advanced NestJS: Dynamic Providers: https://dev.to/nestjs/advanced-nestjs-dynamic-providers-1ee