NestJS 依赖注入是怎么工作的?Module、Provider 和注入机制详解
NestJS 的依赖注入(DI)是从 Angular 借鉴的核心机制。你不需要手动创建实例和传递依赖——在 Provider 里声明,在构造函数里接收,Nest 容器自动装配。Module 是组织 Provider 的边界,控制哪些可以对外暴露、哪些只在内部使用。
依赖注入基本原理
没有 DI 的写法:手动创建依赖,耦合度高。
typescript// 没有 DI const repo = new UserRepository(); const service = new UserService(repo); const controller = new UserController(service);
有 DI 的写法:只声明需要什么,Nest 自动注入。
typescript@Controller('users') export class UsersController { constructor(private usersService: UsersService) {} // 自动注入 }
Nest 的 IoC 容器在启动时扫描所有 Module,根据构造函数的参数类型自动创建和注入实例。private 关键字同时声明和赋值——TypeScript 的参数属性简写。
Module:组织代码的边界
每个 Module 是一个独立的 DI 容器。Module 里注册的 Provider 默认只在 Module 内部可用。
typescript@Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService], // 对外暴露 }) export class UsersModule {}
providers:注册到本 Module 的服务,本 Module 内可注入exports:声明哪些 Provider 可以被其他 Module 使用imports:导入其他 Module 暴露的 Provider
typescript@Module({ imports: [UsersModule], // 导入后可以注入 UsersService providers: [PostsService], }) export class PostsModule {}
如果不 exports: [UsersService],PostsModule 里注入 UsersService 会报错——Nest 找不到这个 Provider。
Provider 的三种注册方式
1. 类名注册(最常见)
typescriptproviders: [UsersService] // 等价于 providers: [{ provide: UsersService, useClass: UsersService }]
2. 值注册(Mock 或配置对象)
typescriptproviders: [{ provide: 'CONFIG', useValue: { dbHost: 'localhost', port: 5432 }, }]
3. 工厂注册(动态创建,依赖其他 Provider)
typescriptproviders: [{ provide: 'DATABASE_CONNECTION', useFactory: (configService: ConfigService) => { return createConnection(configService.get('db')); }, inject: [ConfigService], // 工厂的依赖 }]
注入方式
构造函数注入(推荐):
typescriptconstructor(private usersService: UsersService) {}
属性注入(可选依赖):
typescript@Inject('CONFIG') config: ConfigType<typeof config>;
用 @Inject() 指定 token——当 Provider 不是用类名注册时(字符串 token、Symbol token),必须显式指定。
作用域
默认情况下所有 Provider 是 Singleton(单例)——整个应用共享一个实例。这是最高效的模式,也是 99% 场景的正确选择。
其他作用域:
| 作用域 | 生命周期 | 适用场景 |
|---|---|---|
| DEFAULT(Singleton) | 应用启动时创建,共享 | 几乎所有情况 |
| REQUEST | 每个请求创建新实例 | 需要请求上下文(如租户隔离) |
| TRANSIENT | 每次注入都创建新实例 | 极少用 |
typescript@Injectable({ scope: Scope.REQUEST }) export class RequestLogger {}
不要随意改作用域——REQUEST scope 会显著增加内存和 GC 压力,因为每个请求都要创建和销毁实例。
循环依赖
A 依赖 B,B 依赖 A——Nest 无法确定先创建谁。解决方式:用 forwardRef 延迟解析。
typescript@Module({ imports: [forwardRef(() => ModuleB)], }) export class ModuleA {}
typescriptconstructor( @Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB, ) {}
但循环依赖通常意味着设计有问题——考虑抽取共享逻辑到第三个 Module 里。