依赖注入实战
依赖注入(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。常见于第三方库集成(如数据库、缓存、日志等),可以让模块在不同环境下灵活配置。
动态模块通常通过静态方法 forRoot 或 forFeature 返回一个 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 {}
注意:全局模块只需在根模块导入一次即可,避免重复导入。
常见坑点
- 忘记在模块的
providers或exports中声明服务,导致依赖注入失败。 - 构造函数参数类型写错,NestJS 无法自动注入。
- 跨模块调用服务时,未在
exports导出服务。 - 循环依赖:模块间相互依赖时需注意架构设计,避免死循环。
- 动态模块未正确返回 DynamicModule 对象,或未导出 Provider。
- 全局模块未加
@Global()装饰器,导致服务无法全局注入。 - 滥用全局模块可能导致依赖混乱,建议仅对通用服务使用。
依赖注入让你的代码更易维护、测试和扩展,是 NestJS 推荐的最佳实践。合理利用模块化和依赖注入,可以让你的项目结构更加清晰、健壮和易于扩展。