一、什么是依赖注入?#
DI(Dependency Injection,依赖注入)是一种设计模式。
核心思想:
一个类不负责创建自己的依赖,而是由外部把依赖交给它。
例如,没有 DI 时:
class UserService {
private repository = new UserRepository();
}UserService 自己创建 UserRepository,两者高度耦合。
使用 DI:
@Injectable()
class UserService {
constructor(private readonly repository: UserRepository) {}
}此时 UserService 只声明:
我需要一个
UserRepository。
至于 UserRepository 怎么创建,由 NestJS 负责。
二、为什么 NestJS 要使用 DI?#
DI 主要解决四个问题:
1. 解耦#
业务类不需要自己 new 依赖。
没有 DI:
UserService
↓
new UserRepository()有 DI:
UserService
↓
“我需要 UserRepository”
↓
NestJS Container
↓
提供 UserRepository这样以后可以轻松替换实现。
2. 更容易测试#
没有 DI:
class UserService {
private repository = new UserRepository();
}测试 UserService 时,很难替换真实 Repository。
有 DI:
const mockRepository = {
findById: jest.fn(),
};
const service = new UserService(mockRepository);可以直接传入 Mock。
3. 统一管理对象#
大型项目会有大量对象:
Controller
↓
Service
↓
Repository
↓
Database
↓
Redis
↓
Logger
↓
Config如果全部手动 new,对象之间的创建关系会非常复杂。
NestJS 使用 IoC Container 统一管理这些对象。
4. 管理对象生命周期#
NestJS 不仅负责创建对象,还可以管理对象的生命周期和作用域,例如:
- Singleton
- Request Scope
- Transient Scope
因此 DI 不只是“帮你少写几个 new”。
三、@Injectable() 是什么?#
@Injectable()
export class UserService {}它告诉 Nest:
这个类可以被 Nest 的依赖注入系统管理。
例如:
@Injectable()
export class UserService {
getUser() {
return "Tom";
}
}注意:
@Injectable()本身不是new。
它主要是让 Nest 能够识别并处理这个类。
四、providers 是什么?#
@Module({
providers: [UserService],
})
export class UserModule {}providers 的作用是:
告诉 Nest:请把这些依赖注册到 IoC Container 中。
可以简单理解为:
@Module
↓
providers: [UserService]
↓
注册
↓
IoC Container所以:
providers: [UserService];是 DI 系统非常关键的一步。
五、IoC Container 是什么?#
IoC Container 可以理解成:
NestJS 中负责创建、管理和提供依赖对象的容器。
例如:
IoC Container
│
┌───────────┼───────────┐
↓ ↓ ↓
UserService UserRepository Logger当某个类需要 UserService 时:
constructor(
private readonly userService: UserService,
) {}Nest 会去 Container 中寻找对应的依赖。
大致过程:
UserController
│
│ 需要 UserService
↓
IoC Container
│
│ 查找 UserService
↓
UserService 实例
│
↓
注入 UserController六、constructor 和 DI 的关系#
例如:
@Controller("users")
export class UserController {
constructor(private readonly userService: UserService) {}
}这里不是在调用:
new UserService();而是在声明:
UserController 依赖 UserService。
Nest 根据构造函数中的依赖信息,找到对应的 Provider,然后注入。
最终效果类似于:
const userService = new UserService();
const controller = new UserController(userService);区别是:
这些对象的创建和组装由 NestJS 完成。
七、Token 是什么?#
Token 可以理解为:
依赖对象在 IoC Container 中的“身份证”。
当你写:
providers: [UserService];Nest 可以近似理解为:
{
provide: UserService,
useClass: UserService,
}这里:
Token
↓
UserService所以 UserService 本身就可以作为 Token。
八、为什么需要自定义 Token?#
假设我们定义一个 Repository:
interface UserRepository {
findById(id: number): any;
}然后有两个实现:
UserRepository
│
├── PostgresUserRepository
│
└── MongoUserRepository我们希望 UserService 不关心具体使用哪个数据库。
可以定义 Token:
export const USER_REPOSITORY = "USER_REPOSITORY";然后:
@Module({
providers: [
{
provide: USER_REPOSITORY,
useClass: PostgresUserRepository,
},
],
})
export class UserModule {}此时:
USER_REPOSITORY
↓
PostgresUserRepositoryService:
@Injectable()
export class UserService {
constructor(
@Inject(USER_REPOSITORY)
private readonly repository: UserRepository,
) {}
}意思是:
UserService 需要
USER_REPOSITORY这个 Token 对应的对象。
九、@Inject() 是什么?#
@Inject() 用于:
明确告诉 Nest:我要注入哪个 Token。
例如:
constructor(
@Inject(USER_REPOSITORY)
private readonly repository: UserRepository,
) {}Nest 会根据:
USER_REPOSITORY;去 Container 中寻找对应 Provider。
十、为什么普通 Class 不一定需要 @Inject()?#
如果是:
constructor(
private readonly userService: UserService,
) {}Nest 通常可以根据 TypeScript 的类型元数据知道:
需要什么?
↓
UserService
↓
去 Container 找 UserService所以可以直接注入。
但是如果使用字符串 Token:
"USER_REPOSITORY";Nest 无法仅通过 TypeScript 类型判断具体 Token。
所以需要:
@Inject(USER_REPOSITORY)十一、Provider 的三种常见形式#
1. useClass#
{
provide: USER_REPOSITORY,
useClass: PostgresUserRepository,
}意思:
这个 Token 对应一个类,由 Nest 创建这个类的实例。
关系:
USER_REPOSITORY
↓
PostgresUserRepository2. useValue#
{
provide: USER_REPOSITORY,
useValue: mockRepository,
}意思:
不需要 Nest 创建,直接使用我提供的对象。
特别适合测试:
const mockRepository = {
findById: jest.fn(),
};3. useFactory#
{
provide: 'DATABASE',
useFactory: () => {
return createDatabaseConnection();
},
}意思:
通过一个工厂函数创建依赖。
可以用于复杂的初始化逻辑。
十二、Module 在 DI 中扮演什么角色?#
NestJS 的 Module 可以理解成:
组织和管理依赖的边界。
例如:
@Module({
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService],
})
export class UserModule {}Module 主要关注三个东西:
imports
providers
exportsproviders#
providers: [UserService];表示:
我要让 Nest 管理 UserService。
exports#
exports: [UserService];表示:
UserService 可以被其他 Module 使用。
imports#
imports: [UserModule];表示:
当前 Module 要使用 UserModule 暴露出来的 Provider。
十三、完整 DI 流程#
现在把所有概念串起来。
代码:
@Injectable()
export class UserService {
constructor(
@Inject(USER_REPOSITORY)
private readonly repository: UserRepository,
) {}
}Module:
@Module({
providers: [
UserService,
{
provide: USER_REPOSITORY,
useClass: PostgresUserRepository,
},
],
})
export class UserModule {}Controller:
@Controller("users")
export class UserController {
constructor(private readonly userService: UserService) {}
}NestJS 大致会经历:
UserModule
│
↓
providers
│
┌─────────────┴─────────────┐
↓ ↓
UserService USER_REPOSITORY
│
↓
PostgresUserRepository
│
↓
IoC Container
│
│
UserController ───────需要──────→ UserService
│
↓
Container 查找
│
↓
创建依赖对象
│
↓
注入完成最终类似于:
const repository = new PostgresUserRepository();
const userService = new UserService(repository);
const userController = new UserController(userService);但是这些 new 不需要你自己写。
十四、DI 和 IoC 的关系#
这两个概念很容易混淆。
IoC#
IoC:
Inversion of Control,控制反转。
普通代码:
UserService
↓
自己决定
↓
new Repository()控制权在 UserService。
使用 IoC:
NestJS Container
↓
决定创建什么
↓
Repository
↓
注入 UserService控制权从业务代码转移到了框架。
DI#
DI:
Dependency Injection,依赖注入。
它是实现 IoC 的一种方式。
简单理解:
IoC = 思想
DI = 实现这种思想的一种方式十五、为什么 NestJS 的 DI 很重要?#
因为它让代码从:
自己创建依赖
↓
高度耦合
↓
难以替换
↓
难以测试变成:
声明依赖
↓
Nest Container 管理
↓
Provider 提供实现
↓
依赖注入
↓
低耦合
↓
容易测试和替换十六、最终脑图#
NestJS
│
↓
IoC Container
│
↓
管理 Provider
│
┌────────────┼────────────┐
↓ ↓ ↓
UserService Repository Logger
│
│
constructor(...)
│
↓
声明依赖
│
↓
Nest 查找 Token
│
↓
找到对应 Provider
│
↓
创建/获取实例
│
↓
注入完成而 Module:
Module
│
┌───────────┼───────────┐
↓ ↓ ↓
imports providers exports
│
↓
IoC Container十七、一句话记忆#
把 NestJS DI 记成下面这句话:
constructor声明“我要什么”,providers注册“有什么”,Token标识“我要哪个”,IoC Container 负责“创建和管理”,最后 Nest 把依赖注入进来。
再进一步:
@Injectable()
↓
告诉 Nest:这个类可以被管理
providers
↓
告诉 Nest:把它注册进 Container
Token
↓
告诉 Nest:这个依赖叫什么
@Inject()
↓
告诉 Nest:我要这个 Token
IoC Container
↓
负责创建、管理、查找实例
constructor
↓
声明当前类需要哪些依赖
Module
↓
组织 imports / providers / exports这就是 NestJS Dependency Injection 的完整主线。
