Провайдеры, токены и иерархия инжекторов
Провайдер — это рецепт: он говорит инжектору, что выдать по запросу токена, будь то класс, готовое значение или результат фабрики.
«Токен — это вопрос, провайдер — ответ. DI просто соединяет вопросы с ответами».
Обычно зависимость опознаётся по классу: попросили CartService — получили его. Но иногда нужно внедрить не класс: строку конфигурации, объект настроек, разные реализации интерфейса. Для этого есть провайдеры — способы сказать инжектору, как именно создать значение по токену.
import { InjectionToken } from '@angular/core';
// токен для не-класса (конфиг)
export const API_URL = new InjectionToken<string>('API_URL');
// в bootstrap или providers компонента:
providers: [
{ provide: API_URL, useValue: 'https://api.shop.ru' },
{ provide: CartService, useClass: CartService },
{ provide: Logger, useFactory: () => new ConsoleLogger() },
]
Виды провайдеров: useClass — создать экземпляр класса; useValue — отдать готовое значение; useFactory — позвать функцию и вернуть её результат; useExisting — псевдоним для другого токена. Запросить значение по токену помогает тот же inject():
export class ApiService {
private url = inject(API_URL); // получили строку конфига
}
Как работает под капотом
Инжекторы выстроены в иерархию: корневой инжектор приложения, инжекторы маршрутов, инжекторы компонентов. Когда вы пишете inject(Token), поиск идёт от текущего инжектора вверх к корню, пока не найдётся провайдер. Это позволяет переопределять зависимости локально: компонент с собственным провайдером получит свою версию, а его потомки — унаследуют её.
КОРНЕВОЙ инжектор [Logger: ConsoleLogger]
^
| поиск вверх, если не найдено локально
|
инжектор маршрута [API_URL: 'https://...']
^
|
инжектор компонента [Logger: FakeLogger] <- переопределение
|
inject(Logger) здесь -> FakeLogger (нашли локально, не пошли вверх)
Частые ошибки
- Внедрять интерфейс TypeScript. Интерфейсы исчезают при компиляции; для не-классов нужен
InjectionToken. - Дублировать провайдер в дереве. Локальный провайдер создаёт новый экземпляр — легко случайно потерять синглтон.
- Сложные
useFactoryс побочными эффектами. Фабрика должна быть предсказуемой.
Best practices
- Используйте
InjectionTokenдля конфигов, строк и объектов настроек. - Переопределяйте провайдеры локально только осознанно — например, для тестов или вариаций.
- Предпочитайте
useValueдля констант иuseClassдля подмены реализаций.
Итоги. Провайдеры (useClass/useValue/useFactory) и InjectionToken дают полный контроль над DI, а иерархия инжекторов позволяет переопределять зависимости по дереву. Дальше переходим к реактивности — сигналам.
Закрепляем
Провайдер — это рецепт, по которому инжектор изготавливает значение для токена. Когда зависимость это класс, рецепт тривиален: useClass создаёт его экземпляр. Но реальные приложения часто внедряют не классы: строку базового URL, объект конфигурации, разные реализации одного интерфейса. Для них есть useValue (отдать готовое), useFactory (вычислить функцией) и useExisting (псевдоним). А чтобы у не-класса вообще был ключ для внедрения, его задают через InjectionToken — ведь интерфейсы и примитивы не существуют в рантайме.
Вторая половина темы — иерархия инжекторов. Инжекторы образуют дерево: корневой инжектор приложения, инжекторы маршрутов, инжекторы компонентов. Когда вы запрашиваете зависимость, поиск идёт снизу вверх: от текущего инжектора к корню, и побеждает первый найденный провайдер. Это даёт мощный механизм локального переопределения — компонент может предоставить собственную версию сервиса для себя и своих потомков, не затрагивая остальное приложение. Пользуйтесь этим осознанно: чаще всего вам всё же нужен один синглтон в корне.
| Провайдер | Что выдаёт |
|---|---|
| useClass | Новый экземпляр класса |
| useValue | Готовое значение |
| useFactory | Результат функции |
| InjectionToken | Ключ для внедрения не-класса |