В моем шаблоне 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
.
@JSONDerulo ну, не совсем. Некоторые из них могут быть такими, как вы предложили. Но некоторые из них требуют дополнительной обработки данных и переопределения некоторых реализаций по умолчанию. Поэтому архитектура построена таким образом, что каждая из моделей имеет свой Сервис.
Все пытаются ответить на ваш вопрос, но никто не говорит вам правду: вход – это путь. Я не понимаю, почему вы думаете, что это плохой подход, но именно поэтому входные данные существуют!
@MGX Хотя я согласен, что Input()
— это довольно простой способ решить эту проблему, в некоторых случаях вам может потребоваться, чтобы Служба была доступна в конструкторе Компонента. В противном случае вам просто придется дождаться вызова ngOnInit()
и выполнить неопределенные проверки в шаблоне перед этим вызовом.
@Sulejman, если ваша единственная проблема связана с неопределенными проверками, рассмотрите возможность использования входных данных вместо принятого решения, просто сравните сложность обоих решений...
@MGX Ну, как я уже говорил в посте, я сейчас использую входные данные. Но да, я подумаю о том, чтобы не переходить на этот новый подход, поскольку это кажется ненужной магией оболочки/шаблона. Спасибо за вклад, хотя я действительно думал, что с самого начала сделал это плохо.
Почему бы вам не добавить службу в 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);
}
}
См. руководство по внедрению зависимостей .
Если вы хотите вызывать службу не каждый раз, а по условию, вы можете добавить в компонент входные данные для управления этим.
Та же проблема, что и в другом ответе.
@Sulejman Сулейман, это важная информация, которая должна быть частью вопроса. Однако в данном случае это не имеет большого значения, просто создайте один сервис и добавьте его к поставщикам компонентов. Обновлю свой ответ.
В родительском сервисе вместо 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
.
Предложенное решение не работает, поскольку я заявил, что BaseService — это абстрактный класс. Таким образом, помещение его в массив поставщиков приведет к следующему: Cannot assign an abstract constructor type to a non-abstract constructor type.
@Sulejman обновил мой ответ, вам нужен только один сервис, экземпляры создаются автоматически, когда вы добавляете их в массив поставщиков.
Хорошо, я немного отредактировал вопрос, так как вы могли его пропустить. Обратите внимание, что все реализации на самом деле являются разными сервисами. Я не могу сделать то, что вы предлагаете, потому что все ImplementationService
разные. И не существует ни одного (скажем) ImplementationService1
, от которого все остальные могли бы наследовать, чтобы создать этот ImplementationService
, который вы предлагаете мне использовать в качестве реализации BaseService
.
@Sulejman Тогда почему бы вам не предоставить их как есть, без базового сервиса, здесь вы предоставляете один сервис BaseService
с несколькими реализациями, что не имеет смысла
Подумайте о BaseService
как об интерфейсном сервисе, который я использую в конструкторе CustomTable
. Я не совсем понимаю, как вы предлагаете мне "предоставить их как есть" (может быть, вы можете дать на это другой ответ), потому что в конструкторе Custom table
у меня это определено так: constructor(private service: BaseService) {}
@Sulejman обновил мой ответ, я не думаю, что вам нужно BaseService
, если вы хотите расширить возможности использования BaseService
, например export class ImplementationService1 extends BaseService {
. Но при использовании в провайдерах используйте их напрямую. Вам следует использовать услугу напрямую из @Input
, а не из private service: BaseService
.
Итак, мое исходное/существующее решение (то есть с Input
) является допустимым способом внедрения сервиса? Я имею в виду, что хотел это изменить, потому что мне казалось, что сервисы должны быть всегда доступны в конструкторе. В любом случае спасибо за ваш вклад.
@Sulejman Вы можете это сделать, но это потребует дополнительных затрат на добавление компонента-оболочки, который меняет поставщика, это просто выглядит некрасиво, пожалуйста, четко объясните исходную проблему, возможно, есть более простой способ без такого количества экземпляров службы.
Давайте продолжим обсуждение в чате.
Я старался абстрагировать решение как можно более абстрактно, однако есть предел тому, насколько абстрактные вещи могут оказаться.
В какой-то момент, поскольку вы используете одну и ту же общую таблицу, но с разными сервисами, вам необходимо определить, какой сервис будет использоваться в какой момент.
Благодаря приведенному ниже решению, которое можно найти в этом рабочем stackblitz, конфигурация была перенесена в токен внедрения, а пользовательские компоненты, таким образом, используют 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 {}
Это, пожалуй, самое близкое к решению, которое уже упоминалось . Кроме того, я создал еще одну версию без ServiceToken и InjectionFactory, которая тоже работает. Можете ли вы объяснить причину, по которой вы добавили это вместо того, чтобы просто сделать это, как в моей вилке?
В некоторых комментариях/обсуждениях я читал, что вы не можете внедрить абстрактную службу и получаете сообщение об ошибке, поэтому я выбрал другое решение. Однако, если внедрение абстрактной службы работает для вашего реального решения, это лучше. Код становится намного проще
Я имею в виду, что вы не можете внедрить абстрактную службу напрямую (например: providers: [BaseService]
), но вы можете предоставить реализацию (OneService
или TwoService
) для абстрактной службы через массив providers
(например, providers: [{provide: BaseService, useClass: OneService}]
). В этом случае abstract BaseService
, определенный в конструкторе TableComponent
, просто заполняется OneService
или TwoService
соответственно, поскольку они оба extend BaseService
Согласно обсуждениям в ответах: было бы полезно знать, почему ваши услуги должны быть разных классов? Разве это не может быть один и тот же класс обслуживания с разными настройками для каждого экземпляра? Это сделало бы все намного проще.