NestJS提供者详解:四种注册方式、循环依赖和作用域选择
NestJS 的提供者(Provider)就是"可以被注入的东西"——@Injectable() 装饰的类,通过依赖注入(DI)容器管理生命周期,在控制器或其他服务里通过构造函数参数自动获得实例。服务是最常见的提供者,但提供者不只有服务:配置对象、数据库连接、工厂函数都可以是提供者。
最常用的提供者:服务(Service)
服务封装业务逻辑,控制器只负责接收请求和返回响应:
typescript// users.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class UsersService { private users = []; create(name: string, email: string) { const user = { id: Date.now(), name, email }; this.users.push(user); return user; } findAll() { return this.users; } }
typescript// users.controller.ts @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} // 自动注入 @Post() create(@Body() dto: CreateUserDto) { return this.usersService.create(dto.name, dto.email); } @Get() findAll() { return this.usersService.findAll(); } }
在模块里注册:
typescript@Module({ controllers: [UsersController], providers: [UsersService], // 注册服务,DI 容器会自动创建实例 }) export class UsersModule {}
提供者的四种注册方式
useClass:默认方式
typescriptproviders: [UsersService] // 等价于: providers: [{ provide: UsersService, useClass: UsersService }]
最常用,DI 容器自动 new 一个实例。
useValue:提供常量或外部对象
typescriptproviders: [ { provide: 'API_KEY', useValue: process.env.API_KEY, // 直接给一个值 }, ]
注入时用 @Inject() 指定令牌:
typescriptconstructor(@Inject('API_KEY') private apiKey: string) {}
适合配置值、环境变量、第三方 SDK 实例等不需要 DI 创建的东西。
useFactory:动态创建,可以注入依赖
typescriptproviders: [ { provide: 'DATABASE_CONNECTION', useFactory: (configService: ConfigService) => { return createConnection({ host: configService.get('DB_HOST'), port: configService.get('DB_PORT'), }); }, inject: [ConfigService], // 声明 useFactory 需要哪些依赖 }, ]
useFactory 的参数由 inject 数组提供,DI 容器先解析 inject 里的依赖,再传给工厂函数。适合需要异步初始化、依赖其他服务的场景。
useExisting:别名
typescriptproviders: [ UsersService, { provide: 'IUsersService', // 接口令牌 useExisting: UsersService, // 指向已有的提供者 }, ]
接口在 TypeScript 编译后不存在,不能用 provide: IUsersService,用字符串或 Symbol 令牌 + useExisting 是标准做法。
依赖注入令牌
DI 容器通过令牌(token)匹配依赖。令牌可以是类、字符串或 Symbol:
typescript// 类令牌(最常见) constructor(private usersService: UsersService) {} // 字符串令牌 constructor(@Inject('API_KEY') private apiKey: string) {} // Symbol 令牌(避免命名冲突) export const DATABASE_CONNECTION = Symbol('DATABASE_CONNECTION'); constructor(@Inject(DATABASE_CONNECTION) private db: Connection) {}
类令牌最好用——类型安全,不需要 @Inject() 装饰器。字符串和 Symbol 令牌用在没有对应类的场景。
循环依赖
两个服务互相依赖会报错:Circular dependency detected。
typescript// service-a 依赖 service-b,service-b 又依赖 service-a @Injectable() export class ServiceA { constructor(private serviceB: ServiceB) {} // ❌ 循环依赖 }
解决方案:用 forwardRef 延迟解析:
typescript@Injectable() export class ServiceA { constructor( @Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB, ) {} } @Injectable() export class ServiceB { constructor( @Inject(forwardRef(() => ServiceA)) private serviceA: ServiceA, ) {} }
模块里也要加 forwardRef:
typescript@Module({ imports: [forwardRef(() => ServiceBModule)], }) export class ServiceAModule {}
但 forwardRef 只是绕过了报错,说明设计有问题——更好的做法是提取公共逻辑到第三个服务,或者通过事件解耦。
提供者作用域
默认情况下所有提供者都是单例(Singleton)——整个应用共享一个实例。NestJS 支持三种作用域:
| 作用域 | 生命周期 | 适用场景 |
|---|---|---|
| DEFAULT(单例) | 应用启动时创建,共享 | 几乎所有服务 |
| REQUEST | 每个请求创建一个实例 | 请求上下文数据(如当前用户) |
| TRANSIENT | 每次注入都创建新实例 | 无状态的临时对象 |
typescript@Injectable({ scope: Scope.REQUEST }) export class RequestContextService { private userId: string; setUserId(id: string) { this.userId = id; } getUserId() { return this.userId; } }
注意:REQUEST 作用域的服务,注入它的控制器也必须是 REQUEST 作用域。而且 REQUEST 作用域会显著影响性能——每个请求都创建新实例,数据库连接等资源不能共享。大多数场景用 DEFAULT + 在请求对象上挂数据就够了。
模块间共享提供者
默认情况下,模块的提供者对外不可见。要让其他模块用你的服务,必须 export:
typescript// users.module.ts @Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService], // 暴露给其他模块 }) export class UsersModule {}
其他模块 import 后就能注入 UsersService:
typescript// posts.module.ts @Module({ imports: [UsersModule], // import 整个模块 providers: [PostsService], }) export class PostsModule {} // posts.service.ts @Injectable() export class PostsService { constructor(private usersService: UsersService) {} // 可以用了 }
关键规则:import 模块,注入 export 的服务。不能直接 import 服务,也不能注入没 export 的服务。
可选注入
某些依赖不是必须的,找不到时不报错:
typescriptimport { Optional } from '@nestjs/common'; @Injectable() export class MyService { constructor(@Optional() private cacheService?: CacheService) {} getData() { return this.cacheService?.get('key') ?? this.fetchFromDB(); } }
有 CacheService 就用缓存,没有就直接查数据库。适合功能增强型依赖。