3. 服务与依赖注入:@Injectable 服务提供者、构造函数注入、Provider 作用域与自定义 Provider
在 NestJS 中,依赖注入(Dependency Injection, DI) 是框架的核心设计模式。它允许你将服务、存储库、配置等组件声明为“提供者(Provider)”,并通过构造函数自动注入到需要它们的类中。本章将深入剖析 @Injectable 装饰器、构造函数注入、Provider 作用域以及自定义 Provider 的完整用法。
3.1 @Injectable 服务提供者
@Injectable() 装饰器标记一个类为可以被 Nest 的 IoC(控制反转)容器管理的提供者。任何被装饰的类都可以被注入到其他类中。
// user.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
private users = [];
findAll() {
return this.users;
}
create(user) {
this.users.push(user);
}
}
当 Nest 启动时,它会扫描所有模块的 providers 数组,并将 UserService 注册到 IoC 容器中。默认情况下,Provider 是单例(Singleton)的,即整个应用共享同一个实例。
3.2 构造函数注入
构造函数注入是 NestJS 中最常用的注入方式。通过在类的构造函数中声明参数,Nest 会自动解析并注入对应的依赖。
// user.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
// 构造函数注入:Nest 自动创建 UserService 实例并注入
constructor(private readonly userService: UserService) {}
@Get()
findAll() {
return this.userService.findAll();
}
@Post()
create(@Body() user) {
this.userService.create(user);
}
}
private readonly 或 public readonly 修饰注入的属性。这不仅能确保类型安全,还能防止意外修改依赖。
3.3 Provider 作用域
NestJS 支持三种 Provider 作用域,控制着实例的生命周期和共享范围:
| 作用域 | 枚举值 | 生命周期 | 适用场景 |
|---|---|---|---|
| DEFAULT | Scope.DEFAULT |
单例(整个应用共享一个实例) | 无状态服务、配置服务、数据库连接池 |
| REQUEST | Scope.REQUEST |
每个请求创建一个新实例 | 请求级别的缓存、租户隔离、请求日志 |
| TRANSIENT | Scope.TRANSIENT |
每次注入都创建一个新实例 | 有状态工具类、每次使用都需要独立状态的组件 |
// 设置 REQUEST 作用域
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
private requestId: string;
constructor() {
this.requestId = Math.random().toString(36).substring(7);
}
getRequestId() {
return this.requestId;
}
}
3.4 自定义 Provider
除了使用 @Injectable 类作为 Provider,Nest 还支持多种自定义 Provider 方式,用于处理第三方库、配置值、工厂函数等场景。
3.4.1 值提供者(useValue)
用于注入常量、配置对象或第三方库实例。
// app.module.ts
import { Module } from '@nestjs/common';
const CONFIG = {
apiKey: 'abc123',
endpoint: 'https://api.example.com',
};
@Module({
providers: [
{
provide: 'CONFIG',
useValue: CONFIG,
},
],
})
export class AppModule {}
// 在服务中使用
@Injectable()
export class ApiService {
constructor(@Inject('CONFIG') private config: any) {}
}
3.4.2 工厂提供者(useFactory)
用于需要动态创建或依赖其他 Provider 的场景。
// 工厂提供者示例
import { Module } from '@nestjs/common';
@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: (configService: ConfigService) => {
const connection = createConnection({
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
});
return connection;
},
inject: [ConfigService], // 工厂函数的依赖
},
],
})
export class DatabaseModule {}
3.4.3 别名提供者(useExisting)
用于为一个已有的 Provider 创建别名。
@Module({
providers: [
UserService,
{
provide: 'UserServiceAlias',
useExisting: UserService,
},
],
})
export class UserModule {}
3.5 Provider 注册与注入流程
下面的 SVG 图展示了 NestJS 中 Provider 从注册到注入的完整流程:
3.6 自定义 Provider 对比
| 类型 | 语法 | 适用场景 | 示例 |
|---|---|---|---|
| useValue | { provide: TOKEN, useValue: value } |
注入常量、配置对象、第三方库实例 | 数据库配置、API 密钥 |
| useFactory | { provide: TOKEN, useFactory: factory, inject: [...] } |
需要动态创建或依赖其他 Provider | 数据库连接、HTTP 客户端 |
| useExisting | { provide: TOKEN, useExisting: ExistingProvider } |
为已有 Provider 创建别名 | 接口抽象、多态注入 |
| useClass | { provide: TOKEN, useClass: SomeClass } |
根据环境切换具体实现 | 开发/生产环境的不同服务实现 |
3.7 作用域选择策略
在实际项目中,合理选择 Provider 作用域对性能和正确性至关重要:
- 优先使用 DEFAULT(单例):绝大多数服务应该是无状态的,单例模式性能最优。
- 仅在需要请求隔离时使用 REQUEST:例如多租户应用中的租户上下文、请求级别的日志追踪。
- 谨慎使用 TRANSIENT:每次注入都创建新实例,会增加内存开销。适用于需要独立状态且生命周期极短的工具类。
- 避免作用域冒泡:不要将 REQUEST 作用域的服务注入到单例服务中,否则会引发运行时错误。
3.8 实战:自定义 Provider 实现配置注入
以下示例展示如何使用 useFactory 动态创建数据库连接配置:
// database.providers.ts
import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createConnection } from 'typeorm';
export const databaseProviders: Provider[] = [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const connection = await createConnection({
type: 'mysql',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASS'),
database: configService.get('DB_NAME'),
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: true,
});
return connection;
},
inject: [ConfigService],
},
];
通过自定义 Provider,你可以将复杂的初始化逻辑封装在工厂函数中,同时保持模块的整洁和可测试性。