从零入门 NestJS:构建你的第一个 REST API(五)依赖注入实战

208 阅读6分钟

依赖注入实战

依赖注入(Dependency Injection, DI)是 NestJS 的核心机制之一。它让各层之间解耦、易于测试和复用,是企业级项目架构的基础。通过依赖注入,开发者可以专注于业务逻辑,而不用关心依赖对象的创建和管理,极大提升了代码的可维护性和扩展性。

依赖注入基本概念

NestJS 的依赖注入系统基于 TypeScript 的反射能力,能够自动识别并注入所需的依赖。常见的依赖对象包括 Service、Repository、Factory 等。

  • Provider:Provider 是 NestJS 中可以被注入的对象。只要在模块的 providers 数组中声明,就可以在其他地方通过构造函数注入。
  • @Injectable():通过该装饰器声明的类,NestJS 才能识别并进行依赖注入。
  • 自动注入:NestJS 会根据构造函数参数的类型自动查找并注入对应的 Provider,无需手动实例化。

依赖注入的最大优势在于解耦。比如 Controller 只关心调用 Service 的方法,而不关心 Service 如何实现和实例化,这样便于后期维护和单元测试。

创建服务并注入 Controller

在实际开发中,业务逻辑通常会被封装在 Service 层,Controller 只负责接收请求和返回响应。通过依赖注入,Controller 可以轻松调用 Service 的方法。

// user/user.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  findUser(username: string) {
    // 假设这里有数据库查询
    return { username, email: `${username}@example.com` };
  }
}
// user/user.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {} // 注入 UserService

  @Get('profile')
  getProfile(@Query('username') username: string) {
    return this.userService.findUser(username);
  }
}

通过构造函数注入,UserController 可以直接调用 UserService 的方法,无需手动创建实例。这种方式不仅简化了代码,还方便后续的单元测试(可通过 mock 注入不同实现)。

Provider 注册与模块解耦

在 NestJS 中,Provider 需要在模块的 providers 数组中注册,才能被依赖注入系统识别。通过 exports 导出 Provider,可以让其他模块通过 imports 复用这些服务。

// user/user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService], // 导出供其他模块使用
})
export class UserModule {}
  • providers:声明本模块可注入的服务。
  • exports:允许其他模块通过 imports 依赖本模块并复用服务。

这种模块化设计让大型项目的代码结构更加清晰,便于多人协作和功能复用。

跨模块依赖注入

在实际项目中,常常需要在不同模块之间共享服务。例如,订单模块和消息模块都需要用到用户信息。只需在 app.module.ts 中导入 UserModule,其他模块即可通过依赖注入获取 UserService

// app.module.ts
import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';

@Module({
  imports: [UserModule], // 导入 UserModule
})
export class AppModule {}

通过这种方式,服务的实现细节被隐藏,调用方只需关心接口和功能,极大提升了代码的可维护性。

实战场景:用户服务的注入与调用

假如你有多个业务模块(如订单、消息),都需要用到用户信息,只需在各自模块中通过构造函数注入 UserService,无需重复实现。这样不仅减少了代码冗余,还方便后期统一维护和升级。


动态模块的注入

动态模块是 NestJS 提供的一种高级用法,允许根据不同的配置动态注册 Provider。常见于第三方库集成(如数据库、缓存、日志等),可以让模块在不同环境下灵活配置。

动态模块通常通过静态方法 forRootforFeature 返回一个 DynamicModule 对象,包含需要注册的 Provider 和导出内容。

// logger/logger.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import { LoggerService } from './logger.service';

@Module({})
export class LoggerModule {
  static forRoot(options: { level: string }): DynamicModule {
    return {
      module: LoggerModule,
      providers: [
        LoggerService,
        { provide: 'LOGGER_OPTIONS', useValue: options },
      ],
      exports: [LoggerService],
    };
  }
}
// app.module.ts
import { Module } from '@nestjs/common';
import { LoggerModule } from './logger/logger.module';

@Module({
  imports: [LoggerModule.forRoot({ level: 'debug' })],
})
export class AppModule {}

通过动态模块,可以让服务根据不同配置灵活初始化,适合需要高度可配置的场景。

相互引用模块的注入注意事项

在大型项目中,模块之间有时会出现相互依赖(循环依赖)的情况。例如,订单模块需要用户模块,用户模块又需要订单模块。此时,直接互相 imports 会导致 NestJS 无法解析依赖。

NestJS 提供了 forwardRef 工具来解决循环依赖。通过 forwardRef(() => OtherModule),NestJS 会延迟模块的解析,避免死循环。

// user.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { UserService } from './user.service';
import { OrderModule } from '../order/order.module';

@Module({
  imports: [forwardRef(() => OrderModule)],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

// order.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { OrderService } from './order.service';
import { UserModule } from '../user/user.module';

@Module({
  imports: [forwardRef(() => UserModule)],
  providers: [OrderService],
  exports: [OrderService],
})
export class OrderModule {}

实践建议:尽量避免复杂的循环依赖,必要时可通过抽象接口、事件总线等方式解耦。

动态模块的注册和注入

动态模块不仅可以注册 Provider,还可以根据环境变量、配置文件等动态决定注入内容。常见于数据库、缓存等模块的初始化。

// database/database.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import { DatabaseService } from './database.service';

@Module({})
export class DatabaseModule {
  static forRoot(uri: string): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        DatabaseService,
        { provide: 'DB_URI', useValue: uri },
      ],
      exports: [DatabaseService],
    };
  }
}

// app.module.ts
import { DatabaseModule } from './database/database.module';

@Module({
  imports: [DatabaseModule.forRoot(process.env.DB_URI)],
})
export class AppModule {}

通过这种方式,可以让数据库连接、缓存等服务根据不同环境灵活配置,提升项目的可移植性和扩展性。

全局模块的注入

全局模块(Global Module)是指在整个应用范围内都可用的模块。通过 @Global() 装饰器声明后,模块中的 Provider 会自动注册到全局,无需在每个模块中手动 imports。

这对于一些通用服务(如日志、配置、工具类等)非常有用。

// common/common.module.ts
import { Global, Module } from '@nestjs/common';
import { CommonService } from './common.service';

@Global()
@Module({
  providers: [CommonService],
  exports: [CommonService],
})
export class CommonModule {}

// app.module.ts
import { CommonModule } from './common/common.module';

@Module({
  imports: [CommonModule], // 只需导入一次,所有模块都可注入 CommonService
})
export class AppModule {}

注意:全局模块只需在根模块导入一次即可,避免重复导入。


常见坑点

  • 忘记在模块的 providersexports 中声明服务,导致依赖注入失败。
  • 构造函数参数类型写错,NestJS 无法自动注入。
  • 跨模块调用服务时,未在 exports 导出服务。
  • 循环依赖:模块间相互依赖时需注意架构设计,避免死循环。
  • 动态模块未正确返回 DynamicModule 对象,或未导出 Provider。
  • 全局模块未加 @Global() 装饰器,导致服务无法全局注入。
  • 滥用全局模块可能导致依赖混乱,建议仅对通用服务使用。

依赖注入让你的代码更易维护、测试和扩展,是 NestJS 推荐的最佳实践。合理利用模块化和依赖注入,可以让你的项目结构更加清晰、健壮和易于扩展。