You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular依赖注入:继承与服务的配置问题

Angular 继承式仓库服务的DI配置问题

现有代码结构

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
  ) { }
}

核心疑问

  1. 为何@Injectable()不允许配置providers,必须用@Directive()?
  2. 是否对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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 00:07:57