↓ 跳过正文
  1. 文章/

NestJS 依赖注入(DI)核心原理总结

·2409 字·5 分钟·
hujiacheng
作者
hujiacheng
Front-end Developer / Strive To Become Better
目录

一、什么是依赖注入?
#

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
       ↓
PostgresUserRepository

Service:

@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
       ↓
PostgresUserRepository

2. 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
exports

providers
#

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 的完整主线。

相关文章