
为什么需要依赖注入?
在传统编码模式中,对象需主动创建和管理依赖:
class UserController {
private userService: UserService;
constructor() {
this.userService = new UserService(); // 硬编码依赖
}
}
此方式存在三大痛点:
- 耦合严重:修改依赖需改动业务代码
- 难以维护:无法替换依赖的模拟实现
- 职责混乱:对象需同时承担业务和依赖管理
依赖注入通过控制反转(IoC) 将依赖管理权移交框架,实现高内聚低耦合。
两个核心概念
- 控制反转(IoC):程序控制权从开发者转移至框架
- 依赖注入(DI):通过参数传递依赖(构造函数/属性注入)
依赖注入的关键要素
@Injectable()
标记类可被注入:
@Injectable()
export class UserService {
getUser() { /* ... */ }
}
@Inject()
显式指定注入令牌(Token),用于非类依赖:
constructor(@Inject('CACHE') private cacheService) {}
forwardRef()
解决循环依赖问题(如 A → B → A)
底层实现机制
TypeScript编译时通过emitDecoratorMetadata生成类型信息,运行时由reflect-metadata库读取:
Reflect.getMetadata('design:paramtypes', UserController);
// 输出:[UserService]
IoC 容器工作流
- 依赖声明与元数据收集
- 容器扫描类的构造函数参数,自动为构造函数参数生成元数据
- 容器创建单例实例,按需注入到依赖类中
依赖注入的三种方式
- 构造器注入(主要方案)
constructor(private service: Service)
- 字段(属性)注入
@Inject() private readonly service: Service
- Setter(方法)注入
setService(@Inject() service: Service){{
this.service = service;
}}
自定义Provider
- useFactory 异步初始化或复杂构建
{ provide: Logger, useClass: DevLogger }
- useValue 注入常量或模拟对象
{ provide: 'API_KEY', useValue: '12345' }
- useClass 根据条件切换实现类(如测试环境用 Mock 类)
{ provide: Logger, useClass: DevMock }
- useExisting:允许将一个已注册的 Provider 的实例通过新 Token 暴露给其他模块或类,实现“别名注入”
循环依赖解决方案
循环依赖是指两个或多个模块、类或服务相互直接或间接依赖,形成闭环,导致系统无法正常初始化,这里提供以下几种解决方法:
- 提取公共逻辑到新模块(如 CommonService),打破直接依赖
- 前向引用-forwardRef,通过延迟解析依赖,打破初始化顺序限制
@Module({ imports: [forwardRef(() => AuthModule)] })
...
@Module({ imports: [forwardRef(() => AuthModule)] })
- ModuleRef 手动查找依赖
private userService: UserService;
constructor(private moduleRef: ModuleRef) {}
...
onModuleInit() {
this.userService = this.moduleRef.get(UserService);
}