Injection Tokens, похоже, пугают многих разработчиков. Они не понимают, что это такое, как их использовать и каково их назначение.
Чтобы преодолеть страх перед этой функцией, важно иметь базовое представление о том, как работает инъекция зависимостей Angular. (В отдельной статье мы более подробно рассмотрим внутреннюю работу системы DI в Angular для полного понимания).
Оглавление
Вот пример того, как работает паттерн singleton. Когда мы добавляем @Injectable({providedIn: ‘root’}) к одному из наших сервисов:
@Injectable({ providedIn: 'root' })
export class MyService {
// ...
}
При первом вызове MyService Angular сохранит сервис в записи RootInjector, ключом которой будет класс MyService, а значением - объект, содержащий фабрику MyService, текущее значение и флаг multi.
record:{
//...
[index]:{
key: class MyService,
value: {
factory: f MyService_Factory(t),
multi: undefined
value: {}
}
//...
}
Таким образом, в следующий раз, когда мы захотим внедрить наш сервис через конструктор или функцию inject в другом компоненте, DI Angular будет искать сервис в объекте записи и возвращать его текущее значение или создавать его с помощью функции фабрики.
Также очень важно понять, что произойдет, если мы предоставим наш сервис непосредственно в bootstrapApplication или массиве провайдеров компонента.
Чтобы внедрить MyService, мы напишем следующее:
@Component({
// ...
providers: [MyService],
})
export class AppComponent {}
Этот код является сокращенным синтаксисом для
@Component({
// ...
providers: [{provide: MyService, useClass: MyService}],
})
export class AppComponent {}
Создавая InjectionToken, мы создаем ”статический” ключ для нашей записи или словаря услуг.
Написав это:
export const DATA = new InjectionToken<string>('data');
Мы создаем ”константу” типа string.
InjectionToken может принимать объект в качестве второго параметра, содержащий фабричную функцию и атрибут providedIn (Хотя этот параметр будет устаревшим, так как значением по умолчанию является root и нет другой возможности). Заводская функция действует как значение по умолчанию, если другое значение не предоставлено.
Давайте посмотрим на следующий пример, чтобы прояснить это.
export const DATA = new InjectionToken<string>('data', {
factory: () => 'toto',
});
Angular будет хранить наш токен внутри объекта record объекта RootInjector:
record: {
//...
[index]: {
key: InjectionToken {_desc: 'data', ngMetadataName: 'InjectionToken'}
value: {
factory: () => 'toto',
multi: undefined,
value: "toto"
}
},
//...
}
Затем, когда мы вводим его внутрь компонента, DI Angular будет искать ключ DATA в записи RootInjector и вводить соответствующее значение. (‘toto’ в данном примере)
@Component({
// ...
template: `{{ data }} `, // toto
})
export class AppComponent {
data = inject(DATA);
}
Однако в массиве поставщика компонентов мы можем переопределить этот Token для отображения DATA с другим значением. На этот раз Angular будет хранить значение внутри записи NodeInjector.
@Component({
// ...
provider: [{provide: DATA, useValue: 'titi'}],
template: `{{ data }} `, // titi
})
export class AppComponent {
data = inject(DATA);
}
Когда Angular ищет маркер DATA, он сначала проверяет NodeInjector компонента, затем NodeInjector его родителя и, наконец, RootInjector. В результате, в данном примере выходным значением будет ‘titi’.
Предоставление вашего InjectionToken
useValue
Ключевое слово useValue позволяет нам предоставить строку, число, интерфейс, экземпляр класса или любое постоянное значение. Это очень полезно для настройки свойств конфигурации.
export type Environment = 'local' | 'dev' | 'int' | 'prod';
export interface AppConfig {
version: string;
apiUrl: string;
environment: Environment;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
export const getAppConfigProvider = (value: AppConfig): ValueProvider => ({
provide: APP_CONFIG,
useValue: value,
});
bootstrapApplication(AppComponent, {
providers: [
getAppConfigProvider(environment) // environment files
],
});
В приведенном выше примере мы создаем InjectionToken для хранения конфигурации окружения.
Это рекомендуемый метод доступа к переменным окружения в рабочем пространстве Nx, поскольку он позволяет избежать циклических зависимостей.
Чтобы получить свойства окружения, мы вводим токен внутрь нашего компонента.
@Component({
selector: 'app-root',
standalone: true,
template: `{{ config.version }}`,
})
export class AppComponent {
config = inject(APP_CONFIG);
}
useClass
Ключевое слово useClass позволяет нам инстанцировать новый класс. Это полезно, например, для реализации принципа инверсии зависимостей.
Принцип инверсии зависимостей - это принцип проектирования программного обеспечения, который гласит, что различные уровни зависят от абстракций, а не друг от друга. Такая инверсия делает код более гибким, удобным в обслуживании и тестировании.
export interface Search<T> {
search: (search: string) => T[];
}
export const SEARCH = new InjectionToken<Search<object>>('search');
@Component({
selector: 'shareable',
standalone: true,
imports: [ReactiveFormsModule, NgFor, AsyncPipe, JsonPipe],
template: `
<input type="text" [formControl]="searchInput" />
<button (click)="search()"></button>
<div *ngFor="let d of data | async">{{ d | json }}</div>
`,
})
export class ShareableComponent {
searchService = inject(SEARCH, {optional: true});
data = new BehaviorSubject<object[]>([]);
searchInput = new FormControl('', { nonNullable: true });
// We are not injecting `searchService` with the constructor,
// because `inject` function infers the type.
constructor() {
if (!this.searchService)
throw new Error(`SEARCH TOKEN must be PROVIDED`);
}
search() {
this.data.next(this.searchService.search(this.searchInput.value));
}
}
Компонент ShareableComponent отображает поле ввода, кнопку поиска и список результатов поиска. Когда мы нажимаем кнопку поиска, компонент ищет ”где-то” и выводит результаты. Этот компонент является универсальным и не требует дополнительной информации о том, как и где искать. Детали реализации предоставляются родительским компонентом.
Давайте посмотрим, как мы можем использовать ShareableComponent.
Во-первых, мы создадим служебную функцию для предоставления нашего токена. Это повышает удобство работы разработчика и снижает вероятность ошибок, поскольку добавляет в нашу функцию сильную типизацию.
export const getSearchServiceProvider = <T, C extends Search<T>>(clazz: new () => C): ClassProvider => ({
provide: SEARCH,
useClass: clazz,
});
Используя приведенную выше функцию, мы можем предоставить желаемую реализацию интерфейса Search в компоненте, который вызывает ShareableComponent. Когда Angular попытается внедрить инъекционный токен SEARCH, он будет обходить каждый NodeInjector в дереве компонентов, пока не найдет предоставленную реализацию.
@Injectable()
export class DetailSearchService implements Search<Detail> {
search = (search: string): Detail[] => {
// implementation of our search function
}
}
@Component({
selector: 'parent',
standalone: true,
imports: [ShareableComponent],
providers: [getSearchServiceProvider(DetailSearchService)],
template: `
<shareable></shareable>
`,
})
export class ParentComponent {}
Если мы хотим повторно использовать ShareableComponent с другой реализацией поиска, это становится очень просто.
useFactory
Ключевое слово useFactory позволяет нам предоставить объект через фабричную функцию.
export const USER = new InjectionToken<string>('user');
export const getUserProvider = (index: number): FactoryProvider => ({
provide: USER,
useFactory: () => inject(Store).select(selectUser(index)),
});
Функция inject позволяет нам включать другие сервисы в функцию фабрики.
В качестве альтернативы, если вы не хотите использовать функцию inject, FactoryProvider имеет третий параметр deps, в который можно вводить другие инжектируемые сервисы.
export const getUserProvider = (index: number): FactoryProvider => ({
provide: USER,
useFactory: (store: Store) => store.select(selectUser(index))],
deps: [Store],
});
Но я советую вам использовать функцию inject. Она чище и проще для понимания.
Затем мы можем использовать этот провайдер для получения одного пользователя из хранилища:
@Component({
selector: 'app-root',
standalone: true,
providers: [getUserProvider(2)],
template: ` <div>{{ user }}</div> `,
})
export class ParentComponent {
user = inject(USER);
}
useExisting
Ключевое слово useExisting позволяет нам сопоставить существующий экземпляр сервиса с новым токеном, создавая псевдоним.
При рассмотрении объекта записи, созданного Angular, мы имеем следующее:
record:{
//...
[index]:{
key: InjectionToken {_desc: 'data', ngMetadataName: 'InjectionToken', ɵprov: undefined},
value: {
factory: () => ɵɵinject(resolveForwardRef(provider.useExisting)),
multi: undefined
value: {}
}
//...
}
Angular разрешает InjectionToken, передавая ссылку на класс, указанный в параметре useExisting. Он ищет этот класс в NodeInjector и RootInjector, поэтому он должен быть задан заранее, иначе произойдет ошибка.
NullInjectorError: No provider for XXX!
Notes
При предоставлении InjectionToken через провайдеры не обеспечивается безопасность типов. Например, следующий код не приведет к ошибке Typescript, даже если тип будет определен как число для num , но будет предоставлена строка. (Мы можем получить ошибку во время выполнения).
export const NUMBER = new InjectionToken(‘number’);
@Component({
providers: [{provide: NUMBER, useValue: 'toto'}],
// ...
})
export class ParentComponent {
num = inject(NUMBER);
//^ (property) ParentComponent.num: number
}
Чтобы обеспечить безопасность типов, я советую создать служебную функцию для предоставления вашего маркера.
export const NUMBER = new InjectionToken<number>('number');
export const getNumberProvider = (num: number): ValueProvider => ({
provide: NUMBER,
useValue: num
});
// NUMBER token should be provided through this function
@Component({
providers: [getNumberProvider('toto')],
//^ Argument of type 'string' is not assignable to parameter of type 'number'
// ...
})
export class ParentComponent {
num = inject(NUMBER);
}
Таким образом, наша реализация будет безопасной, и любые ошибки будут возникать во время компиляции, а не во время выполнения.
Вот и все для этой статьи! Теперь у вас должно быть хорошее понимание InjectionToken и того, что они означают. Я надеюсь, что эта информация развенчала их для вас, и вы больше не будете испытывать страх перед ними.
Надеюсь, вы узнали новую концепцию Angular. Если вам понравилось, вы можете найти меня на Twitter или Github.
👉 Если вы хотите ускорить свой путь изучения Angular и Nx, загляните на Angular challenges.