NestJS 进阶:动态提供程序

2025-05-27

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它接受一个可选参数prefixas 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
PREV
不要使用 i18next ❌😢
NEXT
JS 中的 filter、map 和 reduce。何时何地使用?