NestJS 把 Angular 的依赖注入(Dependency Injection,DI)思想搬到了后端。第一次接触时,最困惑的是这些概念:Module(模块)、Provider(提供者)、生命周期,以及它们如何串联起来。

Module:应用的积木块

Module 是 NestJS 的组织单元,类似 React 的组件树,但是是平级的。每个应用至少有一个 AppModule,大型应用会拆分成多个 Module。

一个简单的 UserModule

@Module({
  imports: [DatabaseModule],      // 引入其他 Module
  controllers: [UserController],  // 注册 HTTP 控制器
  providers: [UserService],       // 注册服务
  exports: [UserService],         // 导出给其他 Module 使用
})
export class UserModule {}

关键点

  • imports:引入依赖的 Module(如数据库连接)
  • controllers:处理 HTTP 请求的入口
  • providers:业务逻辑层(Service、Repository 等)
  • exports:把自己的 Provider 暴露给其他 Module

Module 之间的依赖关系

graph LR
    A(AppModule) --> B(UserModule)
    A --> C(AuthModule)
    A --> D(DatabaseModule)
    B --> D
    C --> B
    C --> D
    
    style A fill:#e1f5ff
    style B fill:#f0e1ff
    style C fill:#ffe1f5
    style D fill:#e1ffe1

实际场景

  • AuthModule 需要验证用户,所以 imports UserModule
  • UserModuleAuthModule 都需要数据库,所以 imports DatabaseModule
  • DatabaseModule 是全局依赖,在 AppModule 中一次性导入

全局 Module

@Global()  // 声明为全局 Module
@Module({
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}

加上 @Global() 后,其他 Module 不需要在 imports 中显式引入,可以直接注入 DatabaseService

适用场景:数据库连接、日志服务、配置服务等全局依赖。

DI:依赖注入的魔法

依赖注入解决的是”如何获取依赖”的问题。传统方式是 new Service(),DI 方式是”告诉框架你需要什么,框架自动给你”。

最简单的注入

@Injectable()
export class UserService {
  getUser(id: number) {
    return { id, name: 'Alice' };
  }
}
 
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}
  
  @Get(':id')
  getUser(@Param('id') id: string) {
    return this.userService.getUser(+id);
  }
}

发生了什么

  1. UserService@Injectable() 标记,告诉 NestJS”我可以被注入”
  2. UserModuleproviders 中注册了 UserService
  3. UserControllerconstructor 参数声明了依赖
  4. NestJS 自动创建 UserService 实例并注入

注册 Provider 的三种方式

useClass:注入类实例

最常见的方式,直接写类名:

// 1. 注册:在 Module 中声明
@Module({
  providers: [UserService], // 等价于 { provide: UserService, useClass: UserService }
})
export class UserModule {}
 
// 2. 使用:在 Controller 中注入
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}
}
 
// 测试时替换实现
const module = await Test.createTestingModule({
  providers: [
    UserService,
    { provide: UserRepository, useValue: mockRepo }, // 用 Mock 替换真实依赖
  ],
}).compile();

useValue:注入静态值

适合注入配置项、常量:

// 1. 注册:注入一个静态值
@Module({
  providers: [
    { provide: 'API_URL', useValue: 'https://api.example.com' }
  ],
})
export class AppModule {}
 
// 2. 使用:需要 @Inject() 指定 key
@Injectable()
export class UserService {
  constructor(@Inject('API_URL') private apiUrl: string) {}
  
  fetchData() {
    console.log(`Fetching from ${this.apiUrl}`);
  }
}

useFactory:动态创建实例

适合异步初始化、根据条件创建:

// 1. 注册:用 factory 函数动态创建
@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async (config: ConfigService) => {
        const connection = await createConnection(config.get('DB_URL'));
        return connection;
      },
      inject: [ConfigService], // factory 函数的依赖
    },
    ConfigService,
  ],
})
export class DatabaseModule {}
 
// 2. 使用:注入 factory 创建的实例
@Injectable()
export class UserRepository {
  constructor(@Inject('DATABASE_CONNECTION') private db: any) {}
  
  async findUser(id: number) {
    return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

跨 Module 注入

Service 不仅能在本 Module 内使用,还能导出给其他 Module:

// UserModule:提供 UserService
@Module({
  providers: [UserService],
  exports: [UserService],  // 必须 export
})
export class UserModule {}
 
// AuthModule:使用 UserService
@Module({
  imports: [UserModule],  // 必须 import
  providers: [AuthService],
})
export class AuthModule {}
 
// AuthService 就能注入 UserService 了
@Injectable()
export class AuthService {
  constructor(private readonly userService: UserService) {}
}

三步走

  1. 提供方 exports
  2. 使用方 imports
  3. Service 中才能 constructor 注入

DI 的完整流程

从 Controller 到数据库,依赖关系是这样串起来的:

graph TB
    A[UserController] -->|注入| B[UserService]
    B -->|注入| C[UserRepository]
    C -->|注入| D[DATABASE_CONNECTION]
    
    E[UserModule] -.注册.-> A
    E -.注册.-> B
    E -.注册.-> C
    F[DatabaseModule] -.提供.-> D
    
    style A fill:#e1f5ff
    style B fill:#f0e1ff
    style C fill:#ffe1f5
    style D fill:#e1ffe1

作用域:实例的生命周期

默认情况下,Provider 是单例,整个应用生命周期只创建一次。但可以改成每次请求创建、或每次注入创建:

@Injectable({ scope: Scope.DEFAULT })  // 单例(默认)
export class UserService {}
 
@Injectable({ scope: Scope.REQUEST })  // 每个请求创建新实例
export class RequestContextService {}
 
@Injectable({ scope: Scope.TRANSIENT }) // 每次注入都创建新实例
export class LoggerService {}

REQUEST 作用域的坑:如果一个 Service 依赖了 REQUEST 作用域的 Provider,它自己也会被提升为 REQUEST 作用域,性能会下降。除非需要请求隔离(如请求 ID、用户上下文),否则别用。

生命周期:从启动到关闭

NestJS 应用的生命周期分为三个阶段:初始化 → 运行 → 销毁。每个阶段都有对应的钩子函数。

生命周期钩子

@Injectable()
export class UserService 
  implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy {
  
  onModuleInit() {
    console.log('1. Module 初始化完成');
  }
  
  onApplicationBootstrap() {
    console.log('2. 应用启动完成,可以接收请求');
  }
  
  onModuleDestroy() {
    console.log('3. Module 即将销毁(如关闭数据库连接)');
  }
}

完整的启动顺序

graph TB
    A(创建实例) --> B(依赖注入)
    B --> C(onModuleInit)
    C --> D(onApplicationBootstrap)
    D --> E(应用监听端口)
    E --> F(接收请求)
    F --> G(收到关闭信号)
    G --> H(beforeApplicationShutdown)
    H --> I(onModuleDestroy)
    I --> J(onApplicationShutdown)
    
    style A fill:#e1f5ff
    style C fill:#f0e1ff
    style D fill:#ffe1f5
    style H fill:#ffe1e1
    style I fill:#ffe1e1
    style J fill:#ffe1e1

实际例子:数据库连接管理

@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
  private connection: any;
  
  async onModuleInit() {
    console.log('连接数据库...');
    this.connection = await createConnection({
      host: 'localhost',
      port: 5432,
    });
    console.log('数据库连接成功');
  }
  
  async onModuleDestroy() {
    console.log('关闭数据库连接...');
    await this.connection.close();
    console.log('数据库连接已关闭');
  }
  
  query(sql: string) {
    return this.connection.query(sql);
  }
}

钩子执行顺序

假设有三个 Module:DatabaseModuleUserModuleAppModule

启动时(从依赖到根):

1. DatabaseService.onModuleInit()
2. UserService.onModuleInit()
3. AppModule.onModuleInit()
4. DatabaseService.onApplicationBootstrap()
5. UserService.onApplicationBootstrap()
6. AppModule.onApplicationBootstrap()

关闭时(从根到依赖):

1. AppModule.onModuleDestroy()
2. UserService.onModuleDestroy()
3. DatabaseService.onModuleDestroy()

先初始化底层依赖,再初始化上层;关闭时反过来。

常见陷阱

陷阱 1:循环依赖

// ❌ UserService 和 AuthService 互相依赖
@Injectable()
export class UserService {
  constructor(private authService: AuthService) {}
}
 
@Injectable()
export class AuthService {
  constructor(private userService: UserService) {}
}

解决方法 1:使用 forwardRef()

@Injectable()
export class UserService {
  constructor(
    @Inject(forwardRef(() => AuthService))
    private authService: AuthService
  ) {}
}

解决方法 2(推荐):重新设计依赖关系,提取共享逻辑到第三个 Service。

陷阱 2:忘记 export

// UserModule
@Module({
  providers: [UserService],
  // 忘记 exports: [UserService]
})
export class UserModule {}
 
// AuthModule
@Module({
  imports: [UserModule],
  providers: [AuthService],
})
export class AuthModule {}
 
// ❌ 报错:Nest can't resolve dependencies of AuthService
@Injectable()
export class AuthService {
  constructor(private userService: UserService) {}
}

错误原因UserService 没有被导出,其他 Module 看不到。

陷阱 3:constructor 里做初始化

@Injectable()
export class UserService implements OnModuleInit {
  private client: any;
  
  // ❌ 错误:constructor 里做异步操作
  constructor(private configService: ConfigService) {
    const apiUrl = this.configService.get('API_URL');
    this.client = this.initClient(apiUrl); // ConfigService 可能还没准备好
  }
  
  // ✅ 正确:用生命周期钩子
  async onModuleInit() {
    const apiUrl = this.configService.get('API_URL');
    this.client = await this.initClient(apiUrl);
  }
}

规则constructor 只声明依赖,初始化逻辑放在 onModuleInitonApplicationBootstrap

陷阱 4:REQUEST 作用域会传播

@Injectable({ scope: Scope.REQUEST })
export class RequestService {}
 
@Injectable()  // 默认是单例
export class UserService {
  constructor(private requestService: RequestService) {}
}

结果UserService 会被自动提升为 REQUEST 作用域,每个请求都创建新实例,性能下降。

原因:NestJS 为了保证依赖关系正确,会把依赖链上的所有 Provider 都提升为 REQUEST 作用域。

建议:尽量少用 REQUEST 作用域,或者隔离到单独的 Module。

总结

NestJS 的核心是三个概念:

  1. Module:组织代码的容器,通过 importsexports 建立依赖关系
  2. DI:通过构造函数注入依赖,框架自动管理实例创建
  3. 生命周期:从 onModuleInitonModuleDestroy,掌握钩子时机做初始化和清理

理解这三者的配合,就能快速上手 NestJS。实际开发中:

  • 用 Module 拆分功能模块(User、Auth、Database)
  • 用 DI 降低耦合,方便测试
  • 用生命周期钩子管理资源(数据库连接、定时任务)

想深入了解装饰器的底层机制,可以看Metadata:装饰器背后的魔法标签。配合装饰器流水线,就能理解 NestJS 的完整请求处理机制。