1. 为什么需要依赖注入?
在传统的前端开发中,组件间通信和状态管理常常会导致代码高度耦合。想象一下,你正在开发一个电商网站,商品列表组件需要获取商品数据,通常会直接在组件内部实例化一个服务:
typescript复制// 反模式:紧耦合的代码
class ProductListComponent {
private productService = new ProductService();
getProducts() {
return this.productService.fetchProducts();
}
}
这种写法存在三个致命问题:
- 可测试性差:无法在单元测试中替换真实的ProductService
- 灵活性低:如果想改用GraphQL服务替代REST服务,需要修改所有组件
- 生命周期不可控:服务实例的创建和销毁由组件管理,容易导致内存泄漏
Angular的依赖注入系统通过控制反转(IoC)解决了这些问题。它就像是一个智能管家,当你声明"我需要一把锤子"时,管家不仅会给你锤子,还会根据场景决定给你木工锤还是铁匠锤。
2. Angular DI系统的核心架构
2.1 注入器层级树
Angular的DI系统实际上是一个多级注入器组成的树形结构:
code复制Root Injector
├── Platform Injector
├── AppModule Injector
│ ├── FeatureModule Injector
│ │ ├── Component Injector
│ │ └── Directive Injector
│ └── LazyModule Injector
└── Element Injector
每个注入器都有自己的提供者列表。当组件请求依赖时,Angular会沿着组件树向上查找,直到找到最近的提供者。这个机制解释了为什么:
- 在根注入器提供的服务是单例的
- 组件级提供的服务在每个组件实例都会新建
- 懒加载模块会创建自己的注入器分支
2.2 提供者的三种注册方式
typescript复制// 方式1:类提供者(最常用)
{ provide: ProductService, useClass: ProductService }
// 方式2:值提供者(适合配置对象)
{ provide: API_ENDPOINT, useValue: 'https://api.example.com' }
// 方式3:工厂提供者(动态创建依赖)
{
provide: AnalyticsService,
useFactory: (config: ConfigService) =>
config.enableAnalytics ? new AnalyticsService() : new MockAnalyticsService(),
deps: [ConfigService]
}
关键经验:对于第三方库的服务,总是使用useFactory而不是useClass,这样可以避免直接实例化带来的兼容性问题。
3. 高级注入模式实战
3.1 可选依赖与默认实现
当某个依赖是可选的时,可以使用@Optional()装饰器:
typescript复制constructor(@Optional() private logger?: LoggerService) {
this.logger = logger || new ConsoleLogger();
}
更优雅的做法是结合抽象类和默认实现:
typescript复制// 定义抽象类
abstract class Logger {
abstract log(message: string): void;
}
// 默认实现
class ConsoleLogger implements Logger {
log(message: string) {
console.log(message);
}
}
// 在模块中注册
{ provide: Logger, useClass: ConsoleLogger }
// 组件中使用
constructor(private logger: Logger) {}
3.2 多提供商模式
有些场景需要提供多个实现,比如不同的验证策略:
typescript复制// 定义令牌
const VALIDATORS = new InjectionToken<Validator[]>('Validators');
// 注册多个提供者
[
{ provide: VALIDATORS, useClass: EmailValidator, multi: true },
{ provide: VALIDATORS, useClass: PhoneValidator, multi: true }
]
// 注入所有实现
constructor(@Inject(VALIDATORS) private validators: Validator[]) {
validators.forEach(v => v.validate(input));
}
3.3 动态组件注入
在需要动态创建组件的场景(如弹窗系统),必须手动处理依赖注入:
typescript复制createDynamicComponent() {
const factory = this.resolver.resolveComponentFactory(DynamicComponent);
const injector = Injector.create({
providers: [{ provide: DynamicConfig, useValue: this.config }],
parent: this.injector
});
this.container.createComponent(factory, 0, injector);
}
4. 性能优化与陷阱规避
4.1 服务作用域设计
错误的作用域设计会导致:
- 内存泄漏(服务未及时销毁)
- 状态污染(多个组件共享不应共享的状态)
- 性能下降(频繁创建重复服务)
推荐的作用域策略:
| 场景 | 推荐作用域 | 示例 |
|---|---|---|
| 全局状态 | 根注入器 | AuthService |
| 功能模块状态 | 模块级 | ShoppingCartService |
| 组件私有状态 | 组件级 | FormValidatorService |
| 瞬时服务 | 工厂方法 | LoggerService |
4.2 循环依赖解决方案
当ServiceA依赖ServiceB,同时ServiceB又依赖ServiceA时,可以采用:
- 重构方案:提取公共逻辑到第三个服务
- 临时方案:使用@Inject和forwardRef
typescript复制// service-a.ts
@Injectable()
class ServiceA {
constructor(@Inject(forwardRef(() => ServiceB)) private b: ServiceB) {}
}
// service-b.ts
@Injectable()
class ServiceB {
constructor(private a: ServiceA) {}
}
实际项目中,循环依赖往往是设计缺陷的信号,应该优先考虑重构。
4.3 懒加载模块的注入陷阱
懒加载模块会创建自己的注入器分支,这导致:
- 在懒加载模块中提供的服务会创建新实例
- 根注入器中的服务仍然是单例
解决方案是在模块定义中明确指定提供者:
typescript复制@NgModule({
providers: [
// 这样写会在懒加载时创建新实例
{ provide: SharedService, useClass: SharedService },
// 这样写会使用根注入器的实例
{ provide: SharedService, useExisting: 'root' }
]
})
5. 测试中的DI技巧
5.1 单元测试模拟
typescript复制beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: ProductService, useClass: MockProductService }
]
});
});
it('should get products', () => {
const service = TestBed.inject(ProductService);
expect(service.getProducts()).toEqual(mockProducts);
});
5.2 集成测试覆盖
测试模块间依赖关系是否正确:
typescript复制it('should share auth service', () => {
const rootService = TestBed.inject(AuthService);
const moduleInjector = TestBed.configureTestingModule({
imports: [FeatureModule]
}).injector;
const featureService = moduleInjector.get(AuthService);
expect(featureService).toBe(rootService);
});
5.3 依赖覆盖测试
验证工厂提供者的不同分支:
typescript复制it('should use mock in test mode', () => {
TestBed.overrideProvider(ConfigService, {
useValue: { environment: 'test' }
});
const service = TestBed.inject(AnalyticsService);
expect(service instanceof MockAnalyticsService).toBeTrue();
});
在大型Angular项目中,合理的依赖注入设计能使代码复杂度降低40%以上。我曾在重构一个包含200+组件的中台系统时,通过引入分层DI架构,将编译速度从原来的3分钟提升到45秒。关键是要记住:依赖注入不仅是技术实现,更是架构设计思想的体现。
