@Injectable() 真的只是个标记吗?
在 NestJS 中,@Injectable() 是我们最常接触的装饰器之一。但你是否思考过,它真的只是一个简单的标记吗?它背后隐藏着怎样的机制,让我们的类能够被依赖注入容器识别和管理?
1. 表象:一个简单的装饰器
我们通常这样使用 @Injectable():
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
findAll() {
return ['user1', 'user2'];
}
}然后在其他地方注入使用:
import { Controller, Get } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
findAll() {
return this.userService.findAll();
}
}看起来 @Injectable() 只是告诉 NestJS "这个类可以被注入",但真的是这样吗?
2. 深入源码:Injectable 的本质
让我们来看看 @Injectable() 的实际实现:
// nestjs/common/interfaces/injectable.interface.ts
export interface Injectable {
/**
* Optional name for the injectable
*/
name?: string;
}
// nestjs/common/decorators/core/injectable.decorator.ts
import { SCOPE_OPTIONS_METADATA } from '../constants';
import { Injectable } from '../interfaces/injectable.interface';
import { Type } from '../interfaces/type.interface';
export function Injectable(options?: Injectable): ClassDecorator {
return (target: object) => {
Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
};
}从源码可以看出,@Injectable() 实际上是通过 Reflect.defineMetadata 在类上定义了一些元数据。这说明它不仅仅是一个标记,而是携带了实际信息。
3. 元数据的作用
3.1 Scope 信息
@Injectable() 可以接受一个 options 参数:
@Injectable({ scope: Scope.REQUEST })
export class UserService {
// 每个请求都会创建一个新的实例
}这个 scope 信息会被存储在元数据中,NestJS 容器在创建实例时会读取这个信息来决定如何管理实例的生命周期。
3.2 依赖注入的关键
更重要的是,NestJS 通过 TypeScript 的 emitDecoratorMetadata 功能,在编译时获取构造函数参数的类型信息:
// 当我们写这样的代码时
constructor(private readonly userService: UserService) {}
// TypeScript 会在编译时生成元数据
// Reflect.getMetadata('design:paramtypes', UserController)
// 会返回 [UserService]这就是为什么 NestJS 能够自动注入依赖的关键。
4. 不使用 @Injectable() 会发生什么?
让我们做个实验,去掉 @Injectable() 看看会发生什么:
// 没有 @Injectable()
export class UserService {
findAll() {
return ['user1', 'user2'];
}
}
// 在 controller 中使用
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
findAll() {
return this.userService.findAll();
}
}在这种情况下,如果 UserService 没有被其他方式注册到模块中(比如在 providers 数组中直接列出),NestJS 将无法解析这个依赖,会抛出异常:
Nest can't resolve dependencies of the UserController (?). Please make sure that the argument UserService at index [0] is available in the UserModule context.5. @Injectable() 的真正作用
通过以上分析,我们可以总结出 @Injectable() 的真正作用:
- 标记可注入性:告诉 NestJS 这个类可以作为依赖被注入到其他类中
- 携带元数据:通过
Reflect.defineMetadata存储 scope 等配置信息 - 配合 TypeScript 编译器:与
emitDecoratorMetadata配合,让框架能够获取构造函数参数类型
6. 特殊情况处理
6.1 不需要 @Injectable() 的情况
如果一个类不依赖其他服务,理论上可以不加 @Injectable():
export class MathUtil {
add(a: number, b: number) {
return a + b;
}
}
// 在 service 中使用
@Injectable()
export class UserService {
private mathUtil = new MathUtil(); // 直接实例化
findAll() {
return ['user1', 'user2'];
}
}6.2 必须使用 @Injectable() 的情况
如果一个类需要注入其他服务,就必须使用 @Injectable():
@Injectable()
export class DatabaseService {
// 数据库操作
}
// 必须使用 @Injectable(),因为需要注入 DatabaseService
@Injectable()
export class UserService {
constructor(private readonly databaseService: DatabaseService) {}
findAll() {
// 使用 databaseService 查询用户
return this.databaseService.query('SELECT * FROM users');
}
}7. 最佳实践
7.1 一致使用 @Injectable()
即使一个服务当前不依赖其他服务,也建议加上 @Injectable() 装饰器,这样做的好处是:
- 保持代码一致性
- 为未来可能的依赖注入做准备
- 明确表达这个类是服务类
7.2 合理使用 Scope
默认情况下,NestJS 使用单例模式(Singleton Scope)管理服务实例。只有在特殊情况下才需要改变 scope:
// 请求作用域:每个请求创建一个新实例
@Injectable({ scope: Scope.REQUEST })
export class UserService {
// 适用于需要存储请求特定数据的场景
}
// 临时作用域:每次注入都创建新实例
@Injectable({ scope: Scope.TRANSIENT })
export class UserService {
// 适用于需要完全独立实例的场景
}8. 与模块系统的协同工作
8.1 模块中的提供者注册
@Injectable() 标记的类需要在模块中注册才能被注入:
// user.module.ts
@Module({
controllers: [UserController],
providers: [UserService], // 注册 UserService
})
export class UserModule {}8.2 跨模块注入
通过模块的 exports 机制实现跨模块注入:
// user.module.ts
@Module({
providers: [UserService],
exports: [UserService], // 导出 UserService
})
export class UserModule {}
// auth.module.ts
@Module({
imports: [UserModule], // 导入 UserModule
providers: [AuthService],
})
export class AuthModule {}
// auth.service.ts
@Injectable()
export class AuthService {
constructor(private readonly userService: UserService) {} // 可以注入 UserService
}9. 依赖注入容器的工作原理
9.1 实例创建过程
class InstanceLoader {
async createInstancesOfDependencies() {
// 遍历所有模块
for (const module of this.container.getModules().values()) {
// 创建模块中的提供者实例
await this.createInstancesOfProviders(module);
}
}
private async createInstancesOfProviders(module: Module) {
// 遍历模块中的所有提供者
for (const [token, wrapper] of module.providers) {
// 解析依赖并创建实例
await this.injector.loadInstance(wrapper, module);
}
}
}9.2 依赖解析机制
class Injector {
async resolveComponentInstance<T>(
module: Module,
token: string,
contextId: ContextId,
): Promise<InstanceWrapper<T>> {
// 获取提供者包装器
const wrapper = module.providers.get(token);
// 解析提供者的依赖
const dependencies = await this.resolveDependencies(wrapper, module);
// 创建实例
const instance = await this.instantiateClass(wrapper, dependencies);
return instance;
}
}10. 总结
@Injectable() 远不止是一个简单的标记装饰器。它是 NestJS 依赖注入系统的重要组成部分,通过元数据机制与 TypeScript 编译器配合,实现了自动化的依赖解析和管理。
理解 @Injectable() 的真正作用,有助于我们:
- 更好地设计服务类结构
- 理解依赖注入的工作原理
- 在遇到依赖解析问题时能够快速定位原因
- 合理使用不同的 Scope 来满足业务需求
在下一篇文章中,我们将深入探讨 DI 容器的本质:Map<token, instance>?了解 Nest 的 Container 与 ModuleRef 如何管理实例生命周期。