Angular中Injection Token可解决哪些问题?哪些场景下它不可替代?
下面是几个全局变量/普通类无法同等或更好实现的典型场景:
1. 上下文隔离的多实例注入
全局变量是单一全局作用域的,无法根据组件树或模块层级提供不同值。而Injection Token可以在不同DI层级(根模块、特性模块、组件)提供不同实例,实现上下文隔离。
比如多环境的API地址配置:
// 定义Token export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL'); // 根模块提供生产环境地址 @NgModule({ providers: [ { provide: API_BASE_URL, useValue: 'https://prod-api.example.com' } ] }) export class AppModule {} // 懒加载的测试模块提供测试环境地址 @NgModule({ providers: [ { provide: API_BASE_URL, useValue: 'https://test-api.example.com' } ] }) export class TestModule {}
在组件中注入时,会根据当前组件所在的DI层级获取对应的值:
@Component({ selector: 'app-test' }) export class TestComponent { constructor(@Inject(API_BASE_URL) private baseUrl: string) { console.log(this.baseUrl); // 输出测试环境地址 } }
如果用全局变量,你无法让不同模块的组件拿到不同的API地址,只能靠手动判断模块,代码会变得臃肿且容易出错。
2. 类型安全的原始值/非类依赖注入
全局变量虽然能存储原始值,但无法和Angular的DI系统结合实现类型安全的注入,也无法利用DI的修饰符特性。
比如注入一个主题配置对象:
// 定义Token并指定类型 export const THEME_CONFIG = new InjectionToken<ThemeConfig>('THEME_CONFIG', { providedIn: 'root', factory: () => ({ primaryColor: '#1976d2', darkMode: false }) }); // 组件中类型安全注入 @Component({ selector: 'app-header' }) export class HeaderComponent { constructor(@Inject(THEME_CONFIG) private theme: ThemeConfig) { // theme会自动提示类型属性,不会出现类型错误 this.element.style.backgroundColor = theme.primaryColor; } }
如果用全局变量,你需要手动维护类型,且无法利用DI的providedIn实现树摇(未使用的Token会被打包工具移除),也无法结合@Optional()实现可选注入:
// 可选注入,没有提供值时不会报错 constructor(@Optional() @Inject(THEME_CONFIG) private theme?: ThemeConfig) {}
全局变量做不到这种优雅的可选依赖处理,只能手动判断是否存在,代码冗余。
3. 懒加载模块的封装性依赖
懒加载模块的依赖如果用全局变量,会提前加载到全局作用域,破坏懒加载的封装性和性能优势。而Injection Token的依赖只会在模块加载时才被实例化,且仅在该模块的DI层级可见。
比如一个懒加载的报表模块,需要一个专属的报表服务配置:
export const REPORT_CONFIG = new InjectionToken<ReportConfig>('REPORT_CONFIG'); @NgModule({ providers: [ { provide: REPORT_CONFIG, useFactory: (authService: AuthService) => ({ userId: authService.currentUser.id, reportLimit: 50 }), deps: [AuthService] // 依赖其他服务 } ] }) export class ReportsModule {}
这个配置只会在ReportsModule懒加载时才会通过工厂函数创建,且只有该模块内的组件能获取。如果用全局变量,这个配置会在应用启动时就创建,而且所有组件都能访问,违反了封装原则。
4. 可测试性的无痛替换
测试时,Injection Token可以轻松替换提供的值,不需要修改全局变量,避免测试用例之间的污染。
比如测试上面的HeaderComponent时,替换主题配置:
describe('HeaderComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ providers: [ { provide: THEME_CONFIG, useValue: { primaryColor: '#ff0000', darkMode: true } } ] }).compileComponents(); }); it('should use test theme', () => { const fixture = TestBed.createComponent(HeaderComponent); const component = fixture.componentInstance; expect(component.theme.primaryColor).toBe('#ff0000'); }); });
如果用全局变量,你需要在每个测试用例前后手动重置全局变量的值,容易遗漏导致测试失败,而且代码繁琐。
5. 与Angular DI特性的深度集成
Injection Token可以结合DI的@Self()、@SkipSelf()等修饰符,精确控制依赖的查找范围,这是全局变量完全做不到的。
比如组件只查找自身DI层级的Token,不向上查找父组件或模块:
@Component({ selector: 'app-child', providers: [{ provide: API_BASE_URL, useValue: 'https://child-api.example.com' }] }) export class ChildComponent { constructor(@Self() @Inject(API_BASE_URL) private baseUrl: string) { // 只会获取组件自身提供的地址,无视父层级的配置 } }
内容的提问来源于stack exchange,提问作者Robert Schnitzer

