Skip to content
第 4 章 后端 ⏱ 14 分钟阅读

第 4 章:提供者 ​

学习目标 ​

  • 理解 Provider 的概念
  • 掌握 Service、Factory、Value 三种注册方式
  • 学会构造器注入
  • 避开 3 个依赖注入坑

一、Provider 是什么 ​

NestJS 里一切可被注入的东西都叫 Provider:Service、Repository、Factory、第三方库 client、Config……

typescript
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:

typescript
@Module({
  providers: [UsersService],      // ⚠️ 必须注册才能注入
  controllers: [UsersController],
})
export class UsersModule {}

二、构造器注入(推荐) ​

typescript
// 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) ​

typescript
providers: [UsersService]   // token = UsersService 类

3.2 useClass(动态类) ​

typescript
{
  provide: UsersService,
  useClass: process.env.NODE_ENV === 'test' ? MockUsersService : UsersService,
}

写在哪? —— 写在 Module 的 providers 数组 里,常见两种选择:

方式 1:写在 app.module.ts(根模块) —— 全局生效,最简单:

typescript
// app.module.ts
@Module({
  providers: [
    {
      provide: UsersService,
      useClass: process.env.NODE_ENV === 'test' ? MockUsersService : UsersService,
    },
  ],
})
export class AppModule {}

方式 2:写在业务模块的 providers —— 模块级别隔离,大型项目更推荐:

typescript
// 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 可以传构造参数、做复杂判断、注入其他依赖:

typescript
{
  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(常量) ​

typescript
{
  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 为具体类型:

typescript
// 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(工厂函数) ​

typescript
{
  provide: 'DB_TOKEN',
  useFactory: async () => {
    const conn = await createConnection({ /* ... */ });
    return conn;
  },
}

工厂可以注入其他 Provider:

typescript
{
  provide: 'REPO',
  useFactory: (cfg: ConfigService) => new Repo(cfg.get('DB_URL')),
  inject: [ConfigService],
}

四、自定义 Token ​

typescript
// 定义
export const LOGGER = Symbol('LOGGER');

// 注册
{ provide: LOGGER, useClass: ConsoleLogger }

// 注入
constructor(@Inject(LOGGER) private logger: LoggerService) {}

五、Scope 作用域 ​

Scope说明
DEFAULT单例(整个应用一份)
REQUEST每个请求一份(新实例)
TRANSIENT每次注入都新建
typescript
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {}

@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {}

⚠️ 坑 2:REQUEST scope 会让所有上游变成 request scope,导致性能下降,非必要不用。

六、可选依赖 ​

typescript
constructor(
  @InjectOptional() private cache?: CacheService,
) {}

没注册时 cache 是 undefined,需要做空值判断。

七、循环依赖 ​

typescript
// 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(前向引用),作用是延迟解析:

typescript
@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 拿用户信息

最佳实践(从优到劣):

typescript
// ✅ 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))

八、生命周期钩子 ​

typescript
@Injectable()
export class UsersService implements OnModuleInit, OnModuleDestroy {
  onModuleInit()    { console.log('module init'); }
  onModuleDestroy() { console.log('module destroy'); }
}

四个钩子:OnModuleInit、OnApplicationBootstrap、OnModuleDestroy、BeforeApplicationShutdown。

九、实战:封装 HTTP Client ​

typescript
// 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() 最后手段

动手练习 ​

  1. 写 Logger:用 useValue 注入 { level: 'info' } 的配置到 Service
  2. 写 Factory:用 useFactory 创建带初始数据(数组)的 Service
  3. 写生命周期:在 Service 里实现 onModuleInit 打印一条日志

下一章:第 5 章:模块 →

本站基于 VitePress 构建 · 由 StackHub 团队维护