Провайдеры, токены и иерархия инжекторов

Провайдер — это рецепт: он говорит инжектору, что выдать по запросу токена, будь то класс, готовое значение или результат фабрики.

«Токен — это вопрос, провайдер — ответ. 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Ключ для внедрения не-класса
Проверьте себя
1. Зачем нужен InjectionToken?
AДля ускорения DI
BЧтобы внедрять не-классы (строки, конфиги, объекты), у которых нет типа-класса в рантайме
CЧтобы заменить @Injectable
DДля маршрутизации
2. Что происходит при inject(Token), если провайдер не найден в текущем инжекторе?
AСразу ошибка
BПоиск идёт вверх по иерархии инжекторов к корню, пока провайдер не найдётся
CСоздаётся пустой объект
DБерётся первый попавшийся сервис