一、从紧耦合问题说起
假设我们有一个 UserService 需要发送邮件,通常会直接 new 一个 EmailService:
javascript
// ❌ 紧耦合示例
class EmailService {
send(recipient: string, content: string) { /* ... */ }
}
class UserService {
private emailService = new EmailService(); // 直接创建依赖
register(email: string) {
this.emailService.send(email, 'Welcome');
}
}
问题:
- 难以替换
EmailService的实现(如换成SmsService)
-
测试
UserService时必须真实调用EmailService,无法使用模拟对象。 -
若
EmailService的构造函数发生变化,所有使用它的地方都要修改。
二、控制反转(IoC)
控制反转 :将对象的创建 和依赖查找 的控制权从类内部转移到外部容器(或框架)。
简单说:别找我要依赖,我会告诉你我需要什么,由别人给我。
传统(正向)控制
类主动创建或查找依赖 → 类控制依赖。
类只声明自己需要什么依赖(比如通过构造函数参数),由外部(IoC 容器)在实例化时把依赖"注入"进去 → 控制权反转给了容器。
javascript
// ✅ 符合 IoC 原则:UserService 不关心 EmailService 如何创建
class UserService {
constructor(private emailService: EmailService) {} // 只声明需要
// ...
}
此时谁负责创建 EmailService 并传给 UserService?------ IoC 容器(NestJS 运行时)
三、依赖注入(DI)
依赖注入 是实现 IoC 的一种具体技术。它指:将组件所需的依赖(通常是其他服务或对象)通过构造函数、属性或方法参数的形式注入到组件中,而不是由组件自己创建。
NestJS 主要使用 构造函数注入(官方推荐),也支持属性注入(较少用)。
常见注入方式
-
构造函数注入:依赖在实例化时通过构造函数传入。
-
属性注入 :通过
@Inject()装饰器直接注入到类属性。 -
方法注入:通过方法参数注入(NestJS 不常用)。
四、NestJS 中的依赖注入实现
1. 基础三要素
-
提供者(Provider) :可被注入的类,通常用
@Injectable()装饰。 -
消费者(Consumer):需要依赖的类,在构造函数中声明参数类型。
-
模块(Module) :组织提供者,NestJS 根据模块的
providers数组注册可注入的类。
2. 简单示例
TypeScript
// email.service.ts
import { Injectable } from '@nestjs/common';
@Injectable() // 标记为可注入的提供者
export class EmailService {
send(message: string) {
console.log(`Sending: ${message}`);
}
}
TypeScript
// user.service.ts
import { Injectable } from '@nestjs/common';
import { EmailService } from './email.service';
@Injectable()
export class UserService {
// 构造函数参数中声明依赖,NestJS 会自动注入 EmailService 的实例
constructor(private readonly emailService: EmailService) {}
register() {
this.emailService.send('Welcome');
}
}
TypeScript
// app.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { EmailService } from './email.service';
@Module({
providers: [UserService, EmailService], // 注册所有提供者
})
export class AppModule {}
当 NestJS 启动时:
-
扫描
AppModule的providers,创建每个提供者的实例(默认单例)。 -
创建
UserService时,发现它的构造函数需要EmailService,于是从容器中取出已创建的EmailService实例,注入进去。
3. 注入令牌(Injection Token)
TypeScript
// 使用自定义令牌
import { Inject } from '@nestjs/common';
const EMAIL_SERVICE = 'EMAIL_SERVICE';
@Injectable()
class UserService {
constructor(@Inject(EMAIL_SERVICE) private emailService) {}
}
然后在模块中通过 useClass、useValue、useFactory 等方式提供:
TypeScript
@Module({
providers: [
{
provide: EMAIL_SERVICE,
useClass: EmailService,
},
],
})
export class AppModule {}
4. 作用域(Scope)
NestJS 中的提供者默认是单例(整个应用共享同一个实例)。你也可以改为请求作用域或瞬态作用域:
TypeScript
@Injectable({ scope: Scope.REQUEST }) // 每个请求新建实例
export class RequestScopedService {}
五、为什么要在 NestJS 中使用 DI/IoC?

测试示例(使用 Jest)
TypeScript
describe('UserService', () => {
let userService: UserService;
let mockEmailService: Partial<EmailService>;
beforeEach(async () => {
mockEmailService = { send: jest.fn() };
const module = await Test.createTestingModule({
providers: [
UserService,
{ provide: EmailService, useValue: mockEmailService }, // 注入 mock
],
}).compile();
userService = module.get(UserService);
});
it('should send email on register', () => {
userService.register();
expect(mockEmailService.send).toHaveBeenCalledWith('Welcome');
});
});