Внедрение нескольких реализаций для одного и того же провайдера в Angular

Проблема

В моем шаблоне Angular есть следующие настройки:

<mat-tab-group>
  <mat-tab *ngIf = "sees1">
    <app-custom-table
      (columnButtonClick) = "service1.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees2">
    <app-custom-table>
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees3">
    <app-custom-table>
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees4">
    <app-custom-table>
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees5">
    <app-custom-table
      (columnButtonClick) = "service5.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
</mat-tab-group>

Я хочу, чтобы в каждый из app-custom-table была внедрена отдельная реализация сервиса, чтобы иметь возможность отображать и обрабатывать разные данные.

Идея

На самом деле я пытаюсь изменить существующее решение, которое включало передачу службы в качестве аргумента @Input() непосредственно каждому из компонентов. Вот так:

<mat-tab-group>
  <mat-tab *ngIf = "sees1">
    <app-custom-table [service] = "serviceImpl1"
      (columnButtonClick) = "service1.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees2">
    <app-custom-table [service] = "serviceImpl2">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees3">
    <app-custom-table [service] = "serviceImpl3">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees4">
    <app-custom-table [service] = "serviceImpl4">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees5">
    <app-custom-table [service] = "serviceImpl5"
      (columnButtonClick) = "service5.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
</mat-tab-group>

Я пытаюсь отойти от этого подхода, потому что он кажется плохим.

Я хочу что-то вроде этого:

@Component({
  providers: [
    { provide: BaseService, useClass: ImplementationService1 },
    { provide: BaseService, useClass: ImplementationService2 },
    { provide: BaseService, useClass: ImplementationService3 },
    { provide: BaseService, useClass: ImplementationService4 },
    { provide: BaseService, useClass: ImplementationService5 }]
})

но Angular, очевидно, не знает, куда внедрить каждый из соответствующих сервисов.

Возможно, важно отметить, что все реализации extend класса BaseService представляют собой абстрактный класс с реализацией методов по умолчанию, которые в большинстве случаев не меняются, по крайней мере, для большинства реализаций.

Вопрос

Есть ли способ сообщить Angular, какой сервис следует внедрить, или мне следует придерживаться текущего подхода (внедрение сервиса в качестве аргумента Input())? Неужели нынешний подход настолько плох?

Обновлено:

CustomTable<T> и BaseService<T> являются общими классами, кроме того, BaseService<T> — абстрактный класс без декоратора @Injectable. Все ImplementationService расширяют BaseService конкретной моделью как T.

🤔 А знаете ли вы, что...
Angular Universal позволяет рендерить веб-приложение на сервере для улучшения SEO и производительности.


1
60
3

Ответы:

Почему бы вам не добавить службу в providers компонента app-custom-table, чтобы служба создавалась для каждого экземпляра компонента? Вместо передачи сервисов через входы вы можете просто внедрить и использовать сервис непосредственно в дочернем компоненте, и это будет отдельный экземпляр сервиса для каждого компонента.

@Component({
  selector: 'app-custom-table',
  // this will create a new instance of the service per component instance
  providers: [{ provide: BaseService, useClass: ImplementationService }],
})
export class CustomTableComponent {
  constructor(private service: BaseService) {}

  columnButtonClick(event: unknown) {
    this.service.columnClicked(event);
  }
}

См. руководство по внедрению зависимостей .

Если вы хотите вызывать службу не каждый раз, а по условию, вы можете добавить в компонент входные данные для управления этим.


ОБНОВЛЯТЬ:

В родительском сервисе вместо BaseService используйте разные экземпляры.

@Component({
  providers: [
    ImplementationService1,
    ImplementationService2,
    ImplementationService3,
    ImplementationService4,
    ImplementationService5,
})
export class SomeComponent {

    constructor(
      private impSer1: ImplementationService1
      private impSer2: ImplementationService2
      private impSer3: ImplementationService3
      private impSer4: ImplementationService4
      private impSer5: ImplementationService5
    ) {}

Добавьте их в HTML.

<mat-tab-group>
  <mat-tab *ngIf = "sees1">
    <app-custom-table [service] = "impSer1"
      (columnButtonClick) = "service1.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees2">
    <app-custom-table [service] = "impSer2">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees3">
    <app-custom-table [service] = "impSer3">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees4">
    <app-custom-table [service] = "impSer4">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees5">
    <app-custom-table [service] = "impSer5"
      (columnButtonClick) = "service5.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
</mat-tab-group>

Во-первых, должен быть более простой способ решить то, что вы хотите.

Но на данный момент это мое предложение.

Добавьте сервис в массив поставщиков компонента app-custom-table.

@Component({
  selector: 'app-custom-table',
  ...
  providers: [{ provide: BaseService, useClass: ImplementationService },],
})
export class CustomTable {

Теперь все дочерние компоненты имеют уникальный экземпляр.

Затем возьмите дочерний элемент представления от родительского к дочерним компонентам, чтобы получить текущий экземпляр, для этого вы можете использовать геттер.

...
@ViewChild(CustomTable) customTable: CustomTable;

get baseServiceInstance() {
    return this.customTable?.baseService;
}

Примечание: услуга будет доступна только ngAfterViewInit, поэтому начните использовать эту логику оттуда, а не ngOnInit или constructor.


Решено

Я старался абстрагировать решение как можно более абстрактно, однако есть предел тому, насколько абстрактные вещи могут оказаться.

В какой-то момент, поскольку вы используете одну и ту же общую таблицу, но с разными сервисами, вам необходимо определить, какой сервис будет использоваться в какой момент.

Благодаря приведенному ниже решению, которое можно найти в этом рабочем stackblitz, конфигурация была перенесена в токен внедрения, а пользовательские компоненты, таким образом, используют DI для выполнения вашей работы.

Идея решения заключается в том, чтобы

  • Уменьшите загрязнение от основного компонента-хозяина. Внедрение 6 сервисов, которые будут переданы в качестве входных данных в общий компонент таблицы, может привести к созданию уродливого и неподдерживаемого кода.
  • Ваша многократно используемая таблица может использовать DI для определения экземпляра службы, который ей необходимо использовать.
  • В вашей многоразовой таблице больше нет ненужных полей ввода, которые не связаны с общей функциональностью таблицы.
  • Введение компонентов-оболочек обрабатывает конфигурацию DI, которая будет использоваться его дочерними элементами. Кроме того, название компонента уже намекает на то, какие данные

Аннотация BaseService, согласно вашему требованию, не может быть внедрена.

export abstract class BaseService {

  abstract getData():string;
}

Сервис 1 и Сервис 2, расширение/реализация абстрактного сервиса.

@Injectable({ providedIn: 'root' })
export class OneService extends BaseService {
  override getData(): string {
    return 'One Service';
  }
}

@Injectable({providedIn:'root'})
export class TwoService extends BaseService {
  override getData(): string {
    return 'Two Service';
  }
}

Токен инъекции + заводская функция

export let ServiceToken :InjectionToken<BaseService> = new InjectionToken('service-injection');

export let injectionFactory = (key:string) => {
  switch(key) {
    case "1":
      return inject(OneService);
    case "2":
      return inject(TwoService);
     default:
      throw new Error('not-supported');
  }
};

CustomTable, многоразовый, согласно вашему требованию

@Component({
 standalone:true,
 selector:'my-table',
 template:`
    <div>My Table {{key}}</div>
 `,
})
export class TableComponent {

    constructor(@Inject(ServiceToken) public serivce:BaseService){};

    get key():string {
      return this.serivce.getData();
    }
}

Компоненты-оболочки для таблицы, которые внедряют вашу конкретную службу, которая будет использоваться многоразовой таблицей.

@Component({
  standalone: true,
  selector: 'one-table',
  imports: [TableComponent],
  providers: [{ provide: ServiceToken, useExisting: OneService }],
  template: `
   <my-table>
 `,
})
export class OneTableComponent {}

@Component({
  standalone: true,
  selector: 'two-table',
  imports: [TableComponent],
  providers: [{ provide: ServiceToken, useExisting: TwoService }],,
  template: `
    <my-table>
 `,
})
export class TwoTableComponent {}