Angular依赖注入:继承与服务的配置问题
现有代码结构
export const REPOSITORY_NAME = new InjectionToken<string>('RepositoryName'); export const BASE_ENTITY_NAME = new InjectionToken<string>('BaseEntityName'); @Injectable() export class RepositoryBase<TBaseEntity> { constructor( @Self() @Inject(REPOSITORY_NAME) private _name: string, private _api: ApiBaseService<TBaseEntity> ) {} } @Injectable() export class ApiBaseService<TBaseEntity> { constructor( private _http: HttpClient, @Inject(BASE_ENTITY_NAME) private _baseEntityName: string, ) { } }
尝试的实现
希望创建继承自RepositoryBase的CarRepository并注入所需依赖,编写了如下代码:
@Directive({ // <-------- [1] 此处疑问:为什么必须用@Directive才能配置providers?@Injectable不行吗? providers: [ { provide: BASE_ENTITY_NAME, useValue: 'Car' }, { provide: REPOSITORY_NAME, useValue: 'CarRepository'}, ] }) export class CarRepository extends RepositoryBase<Car> { }
遇到的错误
无论怎么调整(比如尝试@Self()、移除基类@Injectable()、将ApiBaseService设为根注入),都会抛出类似错误:
ERROR NullInjectorError: R3InjectorError(MyFeatureModule)[CarRepository -> InjectionToken RepositoryName]: NullInjectorError: No provider for InjectionToken RepositoryName!
背景需求
- 需要在同一个组件中注入多个仓库服务(如
CarRepository、BicycleRepository等),因此无法在组件级别配置依赖 - 希望
CarRepository能像普通服务一样注入到组件中,而非手动构造实例:
@Component({ template: '...', selector: 'my-component' }) export class MyComponent { constructor( private _carRepository: CarRepository ) { } }
核心疑问
- 为何
@Injectable()不允许配置providers,必须用@Directive()? - 是否对Angular DI的工作原理存在根本性误解?这种场景是否适合用Angular DI实现?
解决方案与解释
1. 为什么@Injectable不能配置providers?
Angular中,@Injectable的核心作用是标记类可被DI系统实例化,它仅通过providedIn属性指定注入器的作用范围(根、模块等),但不支持在装饰器内配置providers数组。只有@Component、@Directive、@NgModule的装饰器可以配置providers——因为这些装饰器对应的类会创建子注入器,属于DI树的节点,能在自身注入器范围内注册依赖。
你之前用@Directive虽然能配置providers,但会把CarRepository标记为指令而非服务,注入时需要指令绑定到组件DOM,这不符合你把它当服务使用的需求,因此会出现依赖找不到的问题。
2. 正确的DI配置方式
要让CarRepository作为服务正常注入,需要在模块的providers数组中完整配置它的依赖链,推荐使用工厂提供者来手动传递基类所需的令牌值:
方式一:工厂函数创建实例
@NgModule({ providers: [ // 配置ApiBaseService<Car>的实例,传入BASE_ENTITY_NAME的值 { provide: ApiBaseService<Car>, useFactory: (http: HttpClient) => new ApiBaseService<Car>(http, 'Car'), deps: [HttpClient] }, // 配置CarRepository的实例,传入REPOSITORY_NAME和ApiBaseService实例 { provide: CarRepository, useFactory: (apiService: ApiBaseService<Car>) => new CarRepository('CarRepository', apiService), deps: [ApiBaseService<Car>] } ] }) export class MyFeatureModule {}
同时,基类RepositoryBase可以移除@Injectable()装饰器——因为我们通过工厂函数手动实例化并传递参数,不需要DI系统自动解析其构造函数依赖。
方式二:令牌关联+模块级依赖注册(单仓库场景适用)
如果只需要单个仓库,且希望保留基类的@Injectable(),可以直接在模块中注册所有依赖:
@Injectable() export class CarRepository extends RepositoryBase<Car> {} @NgModule({ providers: [ CarRepository, ApiBaseService<Car>, { provide: REPOSITORY_NAME, useValue: 'CarRepository' }, { provide: BASE_ENTITY_NAME, useValue: 'Car' } ] }) export class MyFeatureModule {}
注意:这种方式无法同时注入多个仓库,因为同一注入器中同一个令牌(如REPOSITORY_NAME)只能有一个值,会出现依赖冲突。
3. 多仓库场景的优化
如果需要创建多个类似仓库(Car、Bicycle等),可以封装工具函数生成提供者配置,避免重复代码:
import { Type } from '@angular/core'; function createRepositoryProvider<T>( repoClass: Type<RepositoryBase<T>>, repoName: string, entityName: string ) { return [ { provide: ApiBaseService<T>, useFactory: (http: HttpClient) => new ApiBaseService<T>(http, entityName), deps: [HttpClient] }, { provide: repoClass, useFactory: (apiService: ApiBaseService<T>) => new repoClass(repoName, apiService), deps: [ApiBaseService<T>] } ]; } // 在模块中使用 @NgModule({ providers: [ ...createRepositoryProvider(CarRepository, 'CarRepository', 'Car'), ...createRepositoryProvider(BicycleRepository, 'BicycleRepository', 'Bicycle') ] }) export class MyFeatureModule {}
内容的提问来源于stack exchange,提问作者Hafnernuss

