NestJS 进阶:动态提供程序
Livio 是 NestJS 核心团队的成员,也是 @nestjs/terminus 集成的创建者
简介
依赖注入(简称DI)是一种强大的技术,可以以可测试的方式构建松耦合的架构。在 NestJS 中,DI 上下文中的一项称为提供程序 (provider )。提供程序由两部分组成:值 (value) 和唯一令牌 (token)。在 NestJS 中,您可以通过提供程序的令牌来请求其值。使用以下代码片段时,这一点最为明显。
import { NestFactory } from '@nestjs/core';
import { Module } from '@nestjs/common';
@Module({
providers: [
{
provide: 'PORT',
useValue: 3000,
},
],
})
export class AppModule {}
async function bootstrap() {
const app = await NestFactory.createApplicationContext(AppModule);
const port = app.get('PORT');
console.log(port); // Prints: 3000
}
bootstrap();
AppModule
由一个具有令牌的提供程序组成PORT
。
- 我们通过调用来引导我们的应用程序
NestFactory.createApplicationContext
(此方法与 HTTP 实例相同,NestFactory.create
但不会启动 HTTP 实例)。 - 稍后,我们使用 请求提供程序的值。这将按照提供程序中指定的方式
app.get('PORT')
返回。3000
不错。但是,如果你不知道要向用户提供什么怎么办?如果你需要在运行时计算提供程序,该怎么办?
本文将介绍一种我们经常用于各种 NestJS 集成的技术。这项技术将允许您构建高度动态的 NestJS 应用程序,同时仍然利用 DI 的优势。
我们想要实现什么
为了了解动态提供程序的用例,我们将使用一个简单但实用的示例。我们希望有一个名为 的参数装饰器,Logger
它接受一个可选参数prefix
as string
。这个装饰器将注入一个LoggerService
,它将给定的参数添加prefix
到每条日志消息的前面。
所以最终的实现将是这样的:
@Injectable()
export class AppService {
constructor(@Logger('AppService') private logger: LoggerService) {}
getHello() {
this.logger.log('Hello World'); // Prints: '[AppService] Hello World'
return 'Hello World';
}
}
设置 NestJS 应用程序
我们将使用 NestJS CLI 快速入门。如果您尚未安装,请使用以下命令:
npm i -g @nestjs/cli
现在在您选择的终端中运行以下命令来引导您的 Nest 应用程序。
nest new logger-app && cd logger-app
记录器服务
让我们从我们的开始LoggerService
。这项服务稍后会在我们使用@Logger()
装饰器时被注入。我们对这项服务的基本要求是:
- 一种可以将消息记录到标准输出的方法
- 可以设置每个实例的前缀的方法
我们将再次使用 NestJS CLI 来引导我们的模块和服务。
nest generate module Logger
nest generate service Logger
为了满足我们的要求,我们构建了这个最小的LoggerService
。
// src/logger/logger.service.ts
import { Injectable, Scope } from '@nestjs/common';
@Injectable({
scope: Scope.TRANSIENT,
})
export class LoggerService {
private prefix?: string;
log(message: string) {
let formattedMessage = message;
if (this.prefix) {
formattedMessage = `[${this.prefix}] ${message}`;
}
console.log(formattedMessage);
}
setPrefix(prefix: string) {
this.prefix = prefix;
}
}
首先,你可能已经意识到@Injectable()
装饰器使用了 scope 选项Scope.TRANSIENT
。这意味着每次LoggerService
将 注入到我们的应用程序中时,它都会创建该类的一个新实例。由于prefix
属性,这是强制性的。我们不希望只有一个 实例,LoggerService
并且不断地覆盖prefix
选项。
除此之外,LoggerService
应该是不言自明的。
现在我们只需要在中导出我们的服务LoggerModule
,这样我们就可以在中使用它AppModule
。
// src/logger/logger.module.ts
import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}
让我们看看它是否在我们的工作中起作用AppService
。
// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger/logger.service';
@Injectable()
export class AppService {
constructor(private readonly logger: LoggerService) {
this.logger.setPrefix('AppService');
}
getHello(): string {
this.logger.log('Hello World');
return 'Hello World!';
}
}
看起来不错 - 让我们启动应用程序npm run start
并请求网站或在您选择的浏览器中curl http://localhost:3000/
打开。http://localhost:3000
如果一切设置正确,我们将收到以下日志输出。
[AppService] Hello World
这很酷。不过,我们很懒,不是吗?我们不想this.logger.setPrefix('AppService')
在服务的构造函数中显式地写代码?像@Logger('AppService')
之前那样使用logger
-parameter 会简洁得多,而且我们不用每次使用记录器时都定义一个构造函数。
记录器装饰器
就我们的例子而言,我们不需要确切地了解装饰器在 TypeScript 中的工作原理。你只需要知道函数可以作为装饰器来处理。
让我们快速手动创建我们的装饰器。
touch src/logger/logger.decorator.ts
我们只是要重新使用@Inject()
来自的装饰器@nestjs/common
。
// src/logger/logger.decorator.ts
import { Inject } from '@nestjs/common';
export const prefixesForLoggers: string[] = new Array<string>();
export function Logger(prefix: string = '') {
if (!prefixesForLoggers.includes(prefix)) {
prefixesForLoggers.push(prefix);
}
return Inject(`LoggerService${prefix}`);
}
你可以把 想象成@Logger('AppService')
的一个别名@Inject('LoggerServiceAppService')
。我们添加的唯一特殊的东西是一个prefixesForLoggers
数组。我们稍后会用到这个数组。这个数组存储了我们需要的所有前缀。
但是等等,我们的 Nest 应用程序对令牌一无所知LoggerServiceAppService
。因此,让我们使用动态提供程序和新创建的prefixesForLoggers
数组来创建此令牌。
动态提供程序
在本章中,我们将了解如何动态生成提供程序。
我们想
- 为每个前缀创建一个提供程序
- 每个提供商都必须有这样的令牌
'LoggerService' + prefix
- 每个提供者必须调用
LoggerService.setPrefix(prefix)
其实例
- 每个提供商都必须有这样的令牌
为了实现这些要求,我们创建了一个新文件。
touch src/logger/logger.providers.ts
将以下代码复制并粘贴到您的编辑器中。
// src/logger/logger.provider.ts
import { prefixesForLoggers } from './logger.decorator';
import { Provider } from '@nestjs/common';
import { LoggerService } from './logger.service';
function loggerFactory(logger: LoggerService, prefix: string) {
if (prefix) {
logger.setPrefix(prefix);
}
return logger;
}
function createLoggerProvider(prefix: string): Provider<LoggerService> {
return {
provide: `LoggerService${prefix}`,
useFactory: logger => loggerFactory(logger, prefix),
inject: [LoggerService],
};
}
export function createLoggerProviders(): Array<Provider<LoggerService>> {
return prefixesForLoggers.map(prefix => createLoggerProvider(prefix));
}
该createLoggerProviders
函数为装饰器设置的每个前缀创建一个提供程序数组@Logger()
。借助NestJS 的功能,我们可以在提供程序创建之前useFactory
运行该方法。LoggerService.setPrefix()
我们现在需要做的就是将这些记录器提供程序添加到我们的LoggerModule
.
// src/logger/logger.module.ts
import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
import { createLoggerProviders } from './logger.providers';
const loggerProviders = createLoggerProviders();
@Module({
providers: [LoggerService, ...loggerProviders],
exports: [LoggerService, ...loggerProviders],
})
export class LoggerModule {}
就这么简单。等等,不行吗?这根本行不通?因为 JavaScript 的问题,哥们儿。我解释一下:createLoggerProviders
文件加载完成后会立即调用,对吧?到那时,prefixesForLoggers
数组内部会是空的logger.decorator.ts
,因为@Logger()
装饰器还没有被调用。
那么我们如何绕过这个问题呢?答案是“动态模块”。动态模块允许我们通过一个方法创建模块设置(通常作为@Module
装饰器的参数传入)。装饰器调用后,该方法会被调用@Logger
,因此prefixForLoggers
数组将包含所有值。
如果你想进一步了解其工作原理,你可能想看看这个关于 JavaScript 事件循环的视频
因此我们必须将其重写LoggerModule
为动态模块。
// src/logger/logger.module.ts
import { DynamicModule } from '@nestjs/common';
import { LoggerService } from './logger.service';
import { createLoggerProviders } from './logger.providers';
export class LoggerModule {
static forRoot(): DynamicModule {
const prefixedLoggerProviders = createLoggerProviders();
return {
module: LoggerModule,
providers: [LoggerService, ...prefixedLoggerProviders],
exports: [LoggerService, ...prefixedLoggerProviders],
};
}
}
不要忘记更新导入数组app.module.ts
// src/logger/app.module.ts
@Module({
controllers: [AppController],
providers: [AppService],
imports: [LoggerModule.forRoot()],
})
export class AppModule {}
就这样!让我们看看更新后是否能正常工作app.service.ts
// src/app.service.ts
@Injectable()
export class AppService {
constructor(@Logger('AppService') private logger: LoggerService) {}
getHello() {
this.logger.log('Hello World'); // Prints: '[AppService] Hello World'
return 'Hello World';
}
}
调用http://localhost:3000
将给我们以下日志
[AppService] Hello World
耶,我们做到了!
结论
我们已经接触了 NestJS 的许多高级部分。我们已经了解了如何创建简单的装饰器、动态模块和动态提供程序。你可以用它以简洁易测试的方式完成一些令人印象深刻的事情。
@nestjs/typeorm
如上所述,我们对和的内部使用了完全相同的模式@nestjs/mongoose
。例如,在 Mongoose 集成中,我们使用了非常类似的方法为每个模型生成可注入的提供程序。
您可以在这个Github 仓库中找到代码。我还重构了一些小功能并添加了单元测试,因此您可以在生产环境中使用此代码。祝您开发愉快 :)
文章来源:https://dev.to/nestjs/advanced-nestjs-dynamic-providers-1ee