第 4 章:提供者
学习目标
- 理解 Provider 的概念
- 掌握 Service、Factory、Value 三种注册方式
- 学会构造器注入
- 避开 3 个依赖注入坑
一、Provider 是什么
NestJS 里一切可被注入的东西都叫 Provider:Service、Repository、Factory、第三方库 client、Config……
import { Injectable } from '@nestjs/common';
@Injectable() // 标记可注入
export class UsersService {
private users: User[] = [];
findAll() { return this.users; }
create(u: User) { this.users.push(u); return u; }
}注册到 Module:
@Module({
providers: [UsersService], // ⚠️ 必须注册才能注入
controllers: [UsersController],
})
export class UsersModule {}二、构造器注入(推荐)
// users.controller.ts
@Controller('users')
export class UsersController {
// private readonly 自动保存到 this.usersService
constructor(private readonly usersService: UsersService) {}
@Get()
list() { return this.usersService.findAll(); }
}⚠️ 坑 1:参数没用
private readonly→ TS 不报错,但运行期this.usersService是 undefined。
三、三种 Provider 注册方式
3.1 简写(类名即 token)
providers: [UsersService] // token = UsersService 类3.2 useClass(动态类)
{
provide: UsersService,
useClass: process.env.NODE_ENV === 'test' ? MockUsersService : UsersService,
}写在哪? —— 写在 Module 的 providers 数组 里,常见两种选择:
方式 1:写在 app.module.ts(根模块) —— 全局生效,最简单:
// app.module.ts
@Module({
providers: [
{
provide: UsersService,
useClass: process.env.NODE_ENV === 'test' ? MockUsersService : UsersService,
},
],
})
export class AppModule {}方式 2:写在业务模块的 providers —— 模块级别隔离,大型项目更推荐:
// users.module.ts
@Module({
providers: [
{
provide: UsersService,
useClass: process.env.NODE_ENV === 'test' ? MockUsersService : UsersService,
},
],
controllers: [UsersController],
exports: [UsersService],
})
export class UsersModule {}实际项目里更常用
useFactory替代useClass—— 因为useFactory可以传构造参数、做复杂判断、注入其他依赖:
{
provide: 'USERS_SERVICE',
useFactory: (cfg: ConfigService) => {
return process.env.NODE_ENV === 'test'
? new MockUsersService()
: new UsersService(cfg.get('DB_URL'));
},
inject: [ConfigService],
}useClass vs useFactory 选择:
| 场景 | 推荐 |
|---|---|
| 简单二选一(测试/生产) | useClass |
| 需要传构造参数 | useFactory |
| 复杂条件判断 / 读配置 | useFactory |
| 切换整个数据库驱动 | useFactory + ConfigService |
3.3 useValue(常量)
{
provide: 'CONFIG',
useValue: { apiKey: 'xxx', debug: true },
}
// 注入
constructor(@Inject('CONFIG') private cfg: any) {}坑 4:
@Inject('CONFIG') private cfg: any里private必须写(public/protected也行),少了就不是参数属性语法,this.cfg拿不到值。readonly是建议加的,加了后字段不可被重写,更安全。
更推荐的写法 —— 加 readonly + 替换 any 为具体类型:
// config.ts
export interface AppConfig {
apiKey: string;
debug: boolean;
}
export const CONFIG_TOKEN = 'CONFIG';
// 注册
{ provide: CONFIG_TOKEN, useValue: { apiKey: 'xxx', debug: true } as AppConfig }
// 注入
constructor(@Inject(CONFIG_TOKEN) private readonly cfg: AppConfig) {}这样的好处:
cfg.xxx有类型提示和补全- 改字段名时会编译报错
- 防止
this.cfg = ...误改
3.4 useFactory(工厂函数)
{
provide: 'DB_TOKEN',
useFactory: async () => {
const conn = await createConnection({ /* ... */ });
return conn;
},
}工厂可以注入其他 Provider:
{
provide: 'REPO',
useFactory: (cfg: ConfigService) => new Repo(cfg.get('DB_URL')),
inject: [ConfigService],
}四、自定义 Token
// 定义
export const LOGGER = Symbol('LOGGER');
// 注册
{ provide: LOGGER, useClass: ConsoleLogger }
// 注入
constructor(@Inject(LOGGER) private logger: LoggerService) {}五、Scope 作用域
| Scope | 说明 |
|---|---|
DEFAULT | 单例(整个应用一份) |
REQUEST | 每个请求一份(新实例) |
TRANSIENT | 每次注入都新建 |
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {}
@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {}⚠️ 坑 2:
REQUESTscope 会让所有上游变成 request scope,导致性能下降,非必要不用。
六、可选依赖
constructor(
@InjectOptional() private cache?: CacheService,
) {}没注册时 cache 是 undefined,需要做空值判断。
七、循环依赖
// a.service.ts
@Injectable()
export class AService {
constructor(@Inject(forwardRef(() => BService)) private b: BService) {}
}
// b.service.ts
@Injectable()
export class BService {
constructor(@Inject(forwardRef(() => AService)) private a: AService) {}
}⚠️ 坑 3:循环依赖是设计问题,优先考虑提取公共 Service,
forwardRef是最后手段。
forwardRef 详解:
NestJS 实例化流程是递归的:
要创建 AService → 解析 A 的构造器 → 发现需要 BService → 开始创建 BService
→ 发现需要 AService → 又要创建 AService → 死锁(A 还没创建完)forwardRef = forward reference(前向引用),作用是延迟解析:
@Inject(forwardRef(() => BService))
// ^^^^^^^^^^
// 告诉 NestJS:"BService 后面再解析,先注入占位符"实际过程:
1. 先注册 A 和 B(都不实例化)
2. 创建 A,看到 forwardRef → 注入 B 的占位符
3. A 创建好
4. 创建 B,看到 forwardRef → 用已建好的 A 真实实例注入
5. B 创建好,完成什么时候会遇到? 只有 A 依赖 B、B 又依赖 A 这种双向依赖才需要,常见场景:
UsersService调OrdersService查订单OrdersService又回查UsersService拿用户信息
最佳实践(从优到劣):
// ✅ 1. 提取公共 Service(推荐)
A → C(共享逻辑)
B → C
// ✅ 2. 事件 / 中间者模式(彻底解耦)
@Injectable()
export class EventBus {
private listeners = new Map<string, Function[]>();
on(event: string, cb: Function) { /* ... */ }
emit(event: string, data: any) { /* ... */ }
}
// A、B 都只依赖 EventBus,不再互相依赖
// ⚠️ 3. forwardRef(最后手段,能避免就避免)
@Inject(forwardRef(() => BService))八、生命周期钩子
@Injectable()
export class UsersService implements OnModuleInit, OnModuleDestroy {
onModuleInit() { console.log('module init'); }
onModuleDestroy() { console.log('module destroy'); }
}四个钩子:OnModuleInit、OnApplicationBootstrap、OnModuleDestroy、BeforeApplicationShutdown。
九、实战:封装 HTTP Client
// http.service.ts
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
@Injectable()
export class ApiHttpService {
constructor(private readonly http: HttpService) {}
async get<T>(url: string): Promise<T> {
const { data } = await firstValueFrom(this.http.get<T>(url));
return data;
}
}
// app.module.ts
@Module({
imports: [HttpModule.register({ timeout: 5000 })],
providers: [ApiHttpService],
})
export class AppModule {}记忆口诀(Module 三件套):
| 配置项 | 放什么 | 作用 |
|---|---|---|
imports | 第三方 / 其他模块 | 引入后,它们内部的 Provider 自动可用(HttpModule 帮你注册好 HttpService) |
providers | 自己写的类 / 常量 / 工厂 | 自己写的 Provider 必须显式注册才能被注入 |
exports | 当前模块的 Provider | 把 Provider 共享给导入本模块的其他模块(默认只在当前模块可见) |
十、本章小结
| 要点 | 关键 |
|---|---|
| Provider | 一切可注入的对象,@Injectable() 标记 |
| 注册 | Module 的 providers: [] 数组 |
| 注入 | 构造器 + private readonly |
| Token | 类名 / 字符串 / Symbol |
| 工厂 | useFactory + inject[] 支持依赖 |
| Scope | 默认单例,REQUEST/TRANSIENT 慎用 |
| 循环依赖 | forwardRef() 最后手段 |
动手练习
- 写 Logger:用
useValue注入{ level: 'info' }的配置到 Service - 写 Factory:用
useFactory创建带初始数据(数组)的 Service - 写生命周期:在 Service 里实现
onModuleInit打印一条日志
下一章:第 5 章:模块 →