Skip to content

NestJS 提供器

目录

提供器

在 NestJS 中,提供者 是一个可以被注入到其他类中的对象,通常用于封装业务逻辑或复用性强的功能。

Nest 中大多数常见的类都可以被视为提供者,例如:

  • 服务(Services)
  • 仓储类(Repositories)
  • 工厂函数(Factories)
  • 工具类 / 辅助函数(Helpers)

核心理念:依赖注入(Dependency Injection)

Nest 的运行时会自动处理类之间的依赖关系。你只需声明依赖项(如服务),框架就会:

  1. 创建实例
  2. 管理生命周期
  3. 注入到需要的地方

这使得代码高内聚、低耦合,同时便于测试和扩展。

示例:将服务作为提供者注入控制器:

创建 CatsService

ts
// cats.service.ts
import { Injectable } from '@nestjs/common';

@Injectable() // 标记该类为可注入的提供者
export class CatsService {
      private readonly cats = [];

      create(cat) {
           this.cats.push(cat);
      }

      findAll() {
           return this.cats;
      }
}

说明

  • @Injectable() 装饰器表示该类可被注入。
  • 该服务维护一个私有数组,用于模拟存储猫的数据。

控制器中使用 CatsService

ts
// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {} // 注入服务

  @Post()
  create(@Body() cat: any) {
    this.catsService.create(cat);
    return 'Cat created successfully';
  }

  @Get()
  findAll() {
    return this.catsService.findAll();
  }
}

说明

  • CatsController 的构造函数中声明了对 CatsService 的依赖,Nest 会自动注入其实例。
  • 控制器只负责请求路由处理,具体逻辑委托给服务,符合 单一职责原则(SRP)

在模块中注册服务

ts
// app.module.ts 或 cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService], // 注册服务为提供者
})
export class AppModule {}

提示:建议遵循 SOLID 原则

Nest 的模块和提供者系统天然支持面向对象设计中的 SOLID 原则,尤其是:

  • S(Single Responsibility):控制器仅处理 HTTP,服务处理逻辑
  • D(Dependency Inversion):依赖抽象(如接口)而非具体实现
项目作用说明
@Injectable()标记类为可注入的提供者
providers在模块中注册服务等提供者
构造函数注入自动注入依赖,解耦组件间关系
DI 容器Nest 自动管理实例创建与生命周期

SOLID 原则和在NestJS 中的最佳实践

服务

在 NestJS 中,服务类(Service)用于封装和组织业务逻辑,并被控制器或其他服务注入使用。 由于服务通常是单例,它适合作为跨组件共享的依赖项,是典型的 提供者(Provider)

我们先从创建一个简单的 CatsService 开始。该服务用于处理猫相关的数据存储和检索操作,并将被 CatsController 所调用,由于它负责管理业务逻辑,是定义为 提供者(Provider) 的理想选择。

CatsService 实现

ts
// cats.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = []; // 本地内存数组,用作数据存储模拟

  create(cat: Cat) {
    this.cats.push(cat); // 将猫对象添加到数组中
  }

  findAll(): Cat[] {
    return this.cats; // 返回所有已添加的猫
  }
}

关键点说明

  • @Injectable():这是一个装饰器,Nest 用它将类标记为可注入的 提供者。 它告诉 Nest IoC(控制反转)容器可以管理此类的生命周期和依赖关系。
  • cats: Cat[] 是本地内存中的数组,用于存储猫的数据。在实际项目中,这通常会替换为数据库访问逻辑。

快捷提示

使用 Nest CLI 创建服务只需执行以下命令:

bash
nest g service cats

Cat 接口定义

服务中使用的 Cat 接口用于定义猫对象的数据结构:

ts
// interfaces/cat.interface.ts
export interface Cat {
  name: string;
  age: number;
  breed: string;
}

在实际应用中,推荐使用 DTO + 验证 来限制请求体,而接口可用于定义返回类型。

在控制器中使用服务

我们现在将 CatsService 注入到控制器中,处理客户端请求时调用服务逻辑。

ts
// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {} // 依赖注入

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto); // 使用服务处理新增逻辑
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll(); // 使用服务返回所有猫数据
  }
}

// dto/create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}

private catsService: CatsService:这是 TypeScript 的简写写法,它不仅声明了 catsService 属性,还自动将其赋值为构造函数参数中传入的实例(依赖注入)。

控制器通过调用服务的方法,完成业务逻辑的分发,遵循单一职责原则(SRP)

@Body() 自动将请求体中的 JSON 数据映射为 CreateCatDto 实例,需搭配全局 ValidationPipe 实现验证。

依赖注入

Nest 是围绕一种强大的设计模式构建的 — 依赖注入(DI)。 这一模式是 Angular 等大型框架的核心组成部分,也极大增强了模块化、可测试性和可维护性。

👉 建议深入阅读 Angular 官方文档:依赖注入 以了解其设计理念与历史渊源。

在 Nest 中,得益于 TypeScript 的功能,管理依赖非常简单,因为它们是根据其类型解析的。在下面的例子中,Nest 将通过创建并返回 CatsService 的实例来解析 catsService(或者,在单例的情况下,如果已在其他地方请求,则返回现有实例)。然后将此依赖注入到控制器的构造函数中(或分配给指定的属性):

Nest 中的依赖注入实现机制

借助 TypeScript 的类型系统,Nest 能够自动根据类型推断并注入所需的依赖,无需手动注册或配置。

示例:控制器中注入服务

ts
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  // 👇 依赖通过构造函数注入
  constructor(private catsService: CatsService) {}

  @Get()
  findAll() {
    return this.catsService.findAll();
  }
}

注释说明:

代码含义
constructor(private catsService: CatsService)TypeScript 的简写写法,声明并自动赋值 catsService 属性
CatsService 的类型信息Nest 的 IoC 容器会读取此类型,并在模块中查找提供者进行注入
单例机制如果服务类是 @Injectable() 并注册为模块 providers,Nest 会创建一个全局共享实例(singleton)
生命周期默认情况下,Nest 中服务为单例,生命周期与应用相同(除非明确指定为 scope: Scope.REQUEST 等)

注入过程原理(简化理解)

  1. Nest 启动时扫描模块的 providers
  2. 根据构造函数参数的类型签名(如 CatsService),查找对应的提供者
  3. 实例化该服务类(或复用已有实例),注入到控制器的构造函数中
  4. 构造函数被调用,依赖赋值成功,控制器即可使用该服务方法

构造函数自动依赖注入的实现

配套注册:服务必须在模块中注册

ts
// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';

@Module({
  controllers: [CatsController],
  providers: [CatsService], // 👈 注册为提供者,才能注入
})
export class CatsModule {}

小结

概念内容
什么是依赖注入?构造函数或属性中注入外部服务或模块,解耦代码结构
Nest 如何实现?利用 TypeScript 类型系统 + 装饰器,自动构建依赖图并注入
有哪些优点?结构清晰、低耦合、高可测性,支持服务单例/请求级/自定义生命周期
注册规则?任何 @Injectable() 的类都需通过模块中的 providers 数组注册,才可被注入

作用域

在 Nest 中,提供者(Provider)默认具有应用级生命周期,也就是说:

当应用启动时,Nest 会创建每个提供者的实例,并在整个应用生命周期内复用这个实例(即默认是单例)。

同样地:

当应用关闭时,Nest 会销毁这些提供者实例,执行清理逻辑(如果实现了 onModuleDestroy()onApplicationShutdown() 等生命周期钩子)。

默认行为:全局单例作用域(Application Scope)

ts
@Injectable()
export class CatsService {
  // 默认是单例,应用启动时创建一次
}
  • 所有请求、控制器、模块共享同一个 CatsService 实例
  • 适用于无状态或缓存类服务,如日志、数据库连接池、配置服务等

可选行为:请求作用域(Request Scope)

你也可以声明某个服务为“请求作用域”,即:

每次有一个新的 HTTP 请求进来,Nest 会为这个请求重新实例化一份该服务

ts
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
  constructor(@Inject(REQUEST) private request: Request) {}
}
  • 每个请求都独立拥有自己的服务实例
  • 适用于依赖请求上下文的服务(如每个请求用户 ID、请求头)
  • Nest 会自动追踪请求上下文并创建正确实例

实际应用举例

作用域场景示例
Application(默认)共享逻辑缓存、配置、日志、DB连接等
Request与请求状态强相关当前登录用户服务、权限服务
Transient(瞬态)每次注入都新建实例高隔离但代价高,较少使用

TRANSIENT(瞬态) : 没有明确销毁时机,通常随引用释放(即垃圾回收GC)

比喻:一次性笔:你用的时候就造一个,什么时候扔你自己决定

总结

提供者默认是 单例,但可以通过设置作用域变更为 请求级瞬态级别,用于满足不同的生命周期需求。

在多用户请求背景下,请求作用域非常实用,但需注意:性能成本更高,不要滥用。

深入掌握 Scope 的完整机制

自定义提供器

Nest 内部内置了一个 控制反转容器(IoC,Inversion of Control),用于管理应用中各种服务(Provider)之间的关系。

这构成了 Nest 依赖注入机制的基础,但这个机制实际上要比我们之前介绍的更强大得多。你可以通过多种方式定义一个 Provider,比如:

  • 提供一个普通的常量值(useValue
  • 提供一个类(useClass
  • 使用一个同步或异步工厂函数(useFactory + inject

可以用非常灵活的方式注册服务

useClass 示例(默认最常用)

ts
// 提供者
@Injectable()
export class MyService {
  getData() {
    return 'hello from MyService';
  }
}

const MyServiceProvider = {
  provide: 'MyServiceAlias',
  useClass: MyService,
};

注册方式(例如在模块中):

ts
@Module({
  providers: [MyServiceProvider],
  exports: [MyServiceProvider],
})
export class AppModule {}

使用方式

ts
@Injectable()
export class ConsumerService {
  constructor(@Inject('MyServiceAlias') private readonly myService: MyService) {}

  log() {
    console.log(this.myService.getData());
  }
}

useValue 示例(常量)

提供者

ts
const AppConfig = {
  appName: 'Nest Demo',
  version: '1.0.0',
};

const ConfigProvider = {
  provide: 'APP_CONFIG',
  useValue: AppConfig,
};

注册方式

ts
@Module({
  providers: [ConfigProvider],
  exports: [ConfigProvider],
})
export class AppModule {}

使用方式

ts
@Injectable()
export class ConfigService {
  constructor(@Inject('APP_CONFIG') private config: { appName: string }) {}

  print() {
    console.log(`App: ${this.config.appName}`);
  }
}

useFactory 示例(工厂函数 + 注入依赖)

提供者

ts
const LoggerProvider = {
  provide: 'LOGGER',
  useFactory: (config: { logLevel: string }) => {
    return {
      log: (msg: string) => {
        if (config.logLevel === 'debug') {
          console.log(`[DEBUG] ${msg}`);
        }
      },
    };
  },
  inject: ['APP_CONFIG'], // 注入上面定义的 config
};

注册方式(接续 APP_CONFIG

ts
@Module({
  providers: [
    ConfigProvider, // 来自 useValue 示例
    LoggerProvider,
  ],
  exports: ['LOGGER'],
})
export class AppModule {}

使用方式

ts
@Injectable()
export class LogService {
  constructor(@Inject('LOGGER') private logger: { log: (msg: string) => void }) {}

  doSomething() {
    this.logger.log('This is a log message');
  }我们想给它起个别名 ILogger:
}

useExisting 示例(使用别名)

提供者

假设我们已经有一个现成的服务:

ts
@Injectable()
export class RealLoggerService {
  l	og(msg: string) {
    console.log(`[REAL] ${msg}`);
  }
}

我们想给它起个别名 ILogger

ts
const LoggerAliasProvider = {
  provide: 'ILogger',
  useExisting: RealLoggerService,
};

注册方式

ts
@Module({
  providers: [
    RealLoggerService,
    LoggerAliasProvider,
  ],
})
export class AppModule {}

使用方式

ts
@Injectable()
export class SomeService {
  constructor(@Inject('ILogger') private readonly logger: RealLoggerService) {}

  run() {
    this.logger.log('Alias works!');
  }
}
  • providers: [...]在当前模块中注册这个服务
  • exports: [...]把这个服务“导出”给其他模块使用

如果不写exports这个 Provider 只能在 AppModule 这个模块的内部使用,如果你希望 其他模块也能使用这个服务(通过 imports: [AppModule] 引入),你必须把它导出。

ts
@Module({
  providers: [ConfigProvider],
  exports: [ConfigProvider], // 👈 这样别的模块才看得见
})
ts
@Module({
  imports: [AppModule],
})
export class OtherModule {
  constructor(@Inject('APP_CONFIG') private config: any) {} // 只有 export 后这里才能拿到
}

在控制器中使用某个 provider,不需要 exports; ❌ 只有当其他模块 module 也要用这个 provider 时,才需要 exports

只要这个 controller 和 provider 在同一个模块中声明,你就可以直接注入使用,无需 export。

ts
@Module({
  controllers: [AppController],
  providers: [ConfigProvider],
  // ❌ 不需要 exports
})
export class AppModule {}
ts
@Controller()
export class AppController {
  constructor(private readonly config: ConfigProvider) {} // ✅ 可以正常注入
}

其他模块想用这个 provider:

比如你在 AppModule 中注册了 ConfigProvider,但你想在 UserModule 中也使用它,就需要:

ts
@Module({
  providers: [ConfigProvider],
  exports: [ConfigProvider], // 👈 关键是这里
})
export class AppModule {}

然后在别的模块里导入:

ts
@Module({
  imports: [AppModule],
})
export class UserModule {
  constructor(private readonly config: ConfigProvider) {} // ✅ 可以用了
}

所以:providers + exports 一起写,只是为了让它 既能在当前模块内用,也能被其他模块用。如果你不打算跨模块复用,就写 providers 就够了。

服务类的两种注册方式

总结

提供方式说明典型场景
useClass使用某个类作为 Provider默认方式,配合 @Injectable 使用
useValue提供常量(对象/字符串/布尔值)全局配置、枚举、常量数据
useFactory运行时动态创建 Provider 实例依赖注入其他服务、异步初始化
useExisting指向另一个已注册的 Provider别名场景:多个 token 使用同一服务实例

可选提供器

有时候,你的类依赖的某个提供器(比如配置对象)不是必需的,换句话说,即使没有提供这个依赖,也希望程序能继续正常运行。这时我们称这个依赖是“可选的”。

要把某个注入的依赖标记为“可选”,只需在构造函数中使用 @Optional() 装饰器即可。

这个装饰器的作用是:如果 Nest 找不到这个提供器,就不会抛出异常,而是注入 undefined。这时你可以手动在类中处理默认值逻辑。

ts
import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

在这个例子中,HttpService 接受一个泛型的 httpClient 实例作为构造参数,但它是可选的。如果容器中没有绑定 HTTP_OPTIONS 这个自定义令牌,Nest 会注入 undefined,不会报错。

注意两点:

  • 需要用 @Inject('HTTP_OPTIONS') 明确指定注入令牌(因为它不是类型自动解析);
  • 需要用 @Optional() 来容错。

@Inject() 与自定义 token 的组合,在注入非类类型(如对象、函数、配置项)时非常常见。当然,它也可以用于注入类类型,但对于类而言,NestJS 更推荐使用类型注入的方式,例如:constructor(private readonly appService: AppService) {},这种写法更简洁且能自动推断依赖关系。

基于属性的注入

我们之前一直使用的是“基于构造函数的注入”(constructor-based injection),即依赖项通过构造函数参数传入。

在某些特定场景下,另一种形式,基于属性的注入(property-based injection),可能更合适。 例如:当你有一个类继承自父类,而依赖项实际上只在父类中使用,那么你就不得不在子类中写一堆冗余代码将依赖转发到 super()。这时直接在父类属性上注入依赖可以避免这种“构造函数透传”。

NestJS 支持在类的属性上使用 @Inject() 装饰器,直接注入对应的提供器。

ts
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS') private readonly httpClient: T;
}

这里 HttpServicehttpClient 属性通过 @Inject('HTTP_OPTIONS') 直接注入,无需出现在构造函数中。这种方式省略了构造函数注入声明,可以简化继承场景中的代码复杂度。

注意:使用自定义 token 时(如 'HTTP_OPTIONS'),你必须显式使用 @Inject(),Nest 无法自动推断。

如果你的类不扩展其他类,通常最好使用基于构造函数的注入。构造函数明确指定了需要哪些依赖,与使用 @Inject 注释的类属性相比,它提供了更好的可见性并使代码更易于理解。

NestJS 官方并不推荐将属性注入作为主流方式,除非你确实需要绕开构造函数(例如在继承结构复杂时)。

构造函数注入的优势:

  • 依赖一目了然(强制声明在构造函数签名中);
  • 避免依赖“悄悄注入”,提升可维护性;
  • 有助于自动化测试和 IDE 推断。

构造函数透传的冗余感

子类必须接收依赖并手动传递给 super(),哪怕自己并不用。

ts
// common.service.ts
@Injectable()
export class CommonService {
  log(msg: string) {
    console.log(`[CommonService] ${msg}`);
  }
}
ts
// base.service.ts
export class BaseService {
  constructor(protected readonly common: CommonService) {}

  protected report(message: string) {
    this.common.log(message);
  }
}
ts
// user.service.ts
@Injectable()
export class UserService extends BaseService {
  constructor(common: CommonService) {
    super(common); // 👈 必须转发,哪怕这里只是继承用
  }

  findUser() {
    this.report('User found');
  }
}

UserService 根本不关心 CommonService,但却必须在构造函数中声明并传给 super(),显得臃肿、容易出错,维护成本也更高。

基于属性的注入,消除透传

依赖只注入到基类,子类完全不用管,结构更清爽。

ts
// base.service.ts
import { Inject } from '@nestjs/common';
import { CommonService } from './common.service';

export class BaseService {
  @Inject(CommonService)
  protected readonly common: CommonService;

  protected report(message: string) {
    this.common.log(message);
  }
}
ts
// user.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService extends BaseService {
  findUser() {
    this.report('User found');
  }
}

UserService 不需要知道也不关心 CommonService

清晰表达依赖只存在于基类

更符合“关注点分离”(Separation of Concerns)原则。

补充

类继承允许一个类(子类)复用另一个类(父类)中的代码(属性、方法),从而实现代码复用、逻辑共享和行为扩展。

继承的主要作用

作用说明示例
代码复用不用重复写相同逻辑多个子类共用一个 log() 方法
公共逻辑抽象把通用操作集中在父类维护统一错误处理、权限校验
行为扩展(override)子类可改写父类方法,保留核心逻辑的同时定制行为:super.log()handle() 方法在每个服务中有不同实现
分层架构、模块抽象把逻辑结构分清楚,比如 BaseController、AbstractService 等让团队开发更有规范性
统一接口规范父类可定义公共接口,子类必须实现使用抽象类和抽象方法实现类似接口强约束
ts
// 抽象父类:
export abstract class AuditableService {
  logChange(entityName: string, action: string) {
    console.log(`[Audit] ${entityName} ${action}`);
  }
}
ts
// 子类继承父类,复用方法:
@Injectable()
export class UserService extends AuditableService {
  createUser() {
    this.logChange('User', 'created');
  }
}

提供器注册

当你定义了一个服务类(提供器,例如 CatsService),并希望在控制器(如 CatsController)中使用它,Nest 需要知道这个服务的存在。 你必须将服务注册到模块的 providers 数组中,Nest 才能将其实例注入到依赖它的类中。

这是依赖注入系统的入口:告诉 Nest “我要注入谁”

ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}

providers: 所有想要注入到其他地方的服务,都必须注册在这里;

controllers: 控制器也必须显式注册,Nest 才能解析它们。

此结构会告诉 Nest:当你要创建 CatsController 的实例时,它依赖的 CatsService 已经在容器中可用。

此时,我们的目录结构应如下所示:

ts
src
├── cats
│   ├── dto
│   │   └── create-cat.dto.ts
│   ├── interfaces
│   │   └── cat.interface.ts
│   ├── cats.controller.ts
│   └── cats.service.ts
├── app.module.ts
└── main.ts

实践建议:

  • cats 目录聚合所有猫相关代码,形成清晰的模块边界;
  • 拆分 dtointerfaces 有助于结构化数据处理与接口契约;
  • 每个功能模块可拥有自己的 controller、service、module,形成领域划分

手动实例化

Nest 通常自动实例化和注入依赖,无需你操心。但在以下特殊情况下,你可能希望手动获取服务实例

  • main.ts 引导函数中访问服务(如配置服务、初始化逻辑);
  • 动态创建模块或服务;
  • 你正在构建 NestJS 的“插件系统”或“运行时模块”;

你可以通过注入 ModuleRef 类手动获取某个已注册的提供器

ts
import { Injectable, ModuleRef } from '@nestjs/core';

@Injectable()
export class ManualService {
  constructor(private moduleRef: ModuleRef) {}

  async doSomething() {
    const catsService = await this.moduleRef.resolve(CatsService);
    catsService.findAll();
  }
}

moduleRef.resolve() 会从 Nest 容器中取出实例,适用于动态场景下的依赖获取。

在 bootstrap() 中访问提供器(独立应用场景)

ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const configService = app.get(ConfigService); // 手动获取配置服务

  await app.listen(3000);
}

这非常常见,尤其是你需要在启动期间读取配置、连接数据库、注册全局中间件时。

大部分业务服务注册使用 @Injectable() + 模块中的 providers 数组,在构造函数中注入依赖使用类型注入,特殊启动场景访问服务(如 ConfigService)使用 app.get(),动态或懒加载服务实例使用 ModuleRef.resolve()

总结:NestJS 的依赖注入默认是自动的,但你仍然可以在必要时通过 ModuleRefapp.get() 进行手动控制,这提供了灵活性但要谨慎使用。

从技术上讲:只要你注入了 ModuleRef,你就可以在任何地方动态解析其他提供器(服务类),但是滥用ModuleRef会导致:模块之间高耦合、失去边界等.....

所以,它只适合那些真的需要“动态”或“懒加载”的特殊场景

场景是否推荐使用 ModuleRef.resolve()说明
✅ 动态选择服务实现(策略模式)✔️ 推荐根据用户请求或配置决定注入哪个服务
✅ 懒加载重型服务(仅用时才实例化)✔️ 推荐比如生成报表、调用外部服务,避免启动时全部初始化
✅ 插件系统(运行时加载新模块)✔️ 推荐动态加载某插件模块并调用其服务
✅ CLI 脚本 / 定时任务脚本✔️ 可用独立调用服务执行任务逻辑
❌ 普通业务逻辑中替代构造函数注入❌ 不推荐会造成依赖隐蔽,破坏架构规范
❌ 控制器中直接调用其他服务(非解耦)❌ 不推荐应该通过构造函数注入依赖,不要“偷取服务”

场景:根据用户角色动态选择处理器

定义两个服务实现

ts
@Injectable()
export class AdminProcessor {
  process() {
    console.log('Admin 处理逻辑');
  }
}

@Injectable()
export class UserProcessor {
  process() {
    console.log('User 处理逻辑');
  }
}

写一个动态服务管理器(用 ModuleRef

ts
@Injectable()
export class ProcessorManager {
  constructor(private readonly moduleRef: ModuleRef) {}

  async handle(role: 'admin' | 'user') {
    const ServiceClass = role === 'admin' ? AdminProcessor : UserProcessor;
    const instance = await this.moduleRef.resolve(ServiceClass);
    instance.process();
  }
}

控制器调用

ts
@Controller()
export class AppController {
  constructor(private readonly manager: ProcessorManager) {}

  @Get('/process/:role')
  async handle(@Param('role') role: 'admin' | 'user') {
    await this.manager.handle(role);
  }
}

访问 /process/admin 就会输出 Admin 处理逻辑,这个服务是 按需实例化的

场景:懒加载大内存服务

假设你有一个服务在启动时很重,但实际只在运行时偶尔用:

ts
@Injectable()
export class PdfRenderService {
  constructor() {
    console.log('PDF 引擎初始化中... 🐌');
  }

  render() {
    return '已生成 PDF 文件';
  }
}

使用懒加载:

ts
@Injectable()
export class ReportService {
  constructor(private readonly moduleRef: ModuleRef) {}

  async generateReport() {
    const pdfService = await this.moduleRef.resolve(PdfRenderService);
    return pdfService.render();
  }
}

好处:启动时不会初始化 PdfRenderService,直到你 generateReport() 才加载。

最后更新时间:

如有转载或 CV 的请标注本站原文地址