Angular

InjectionToken и провайдеры

Сервис-класс провайдится просто: имя класса само работает ключом в инжекторе. Но как заинжектить строку с URL API, объект конфигурации или интерфейс, у которого в рантайме нет класса? У TypeScript-интерфейса нет рантайм-представления, а у строки нет уникальности. Здесь появляется InjectionToken - уникальный ключ, который Angular умеет искать в дереве инжекторов так же, как класс. Вокруг него выстроены пять рецептов провайдеров, на которых держится конфигурация всех крупных Angular-приложений.

  • Angular Material: токены вроде MAT_DATE_LOCALE и MAT_FORM_FIELD_DEFAULT_OPTIONS настраивают компоненты библиотеки через DI без правки исходников
  • @angular/common: токен LOCALE_ID управляет локалью пайпов date и number во всём приложении
  • HTTP-стек: HTTP_INTERCEPTORS - классический multi-провайдер, собирающий цепочку перехватчиков в один массив
  • Конфиг окружения: команды отдают base URL и feature-флаги через InjectionToken вместо глобальных переменных, чтобы их можно было подменить в тестах
  • NgRx и сторонние библиотеки: useFactory собирает сторы и эффекты из зависимостей, известных только в рантайме

Предварительные знания

  • Понимание Dependency Injection: что инжектор хранит и выдаёт зависимости по ключу
  • Функция inject() и провайдинг сервиса через providedIn: 'root'
  • Базовые дженерики TypeScript и разница между типом времени компиляции и значением рантайма

Зачем нужен InjectionToken

Инжектор Angular - это карта ключ-значение. Когда зависимость это класс, ключом служит сама ссылка на класс: она уникальна и существует в рантайме. Проблема начинается, когда нужно заинжектить не класс. TypeScript-интерфейс исчезает при компиляции, поэтому ключом быть не может. Строка вроде 'apiUrl' существует в рантайме, но не уникальна: две библиотеки легко используют одну и ту же строку и перетрут конфиги друг друга.

Объект APP_CONFIG - это и есть ключ. Строка 'app.config' внутри нужна только для понятных сообщений об ошибках, уникальность даёт сама ссылка на объект. Дженерик <AppConfig> привязывает к токену тип: inject(APP_CONFIG) вернёт значение, типизированное как AppConfig, без приведений.

Параметр factory вместе с providedIn: 'root' делает токен tree-shakable: если на RETRY_LIMIT нет ни одной ссылки, сборщик выкинет его из бандла. Это рекомендованный способ задавать значения по умолчанию для библиотечных токенов.

Почему для значения, описанного TypeScript-интерфейсом, нельзя использовать сам интерфейс как ключ DI?

Пять рецептов провайдеров

Провайдер отвечает на вопрос: что инжектор вернёт по данному ключу. Сокращённая запись providers: [LoggerService] разворачивается в полную: { provide: LoggerService, useClass: LoggerService }. Рецепт - это поле use*, которое задаёт способ получения значения. Их четыре основных плюс краткая форма.

РецептЧто делаетТипичный случай
useClassСоздаёт экземпляр указанного классаПодмена реализации: реальный сервис на мок в тесте
useValueВозвращает готовое значение как естьОбъект конфигурации, константа, заранее собранный объект
useFactoryВызывает функцию и возвращает её результатЗависимость зависит от других зависимостей или рантайм-условий
useExistingАлиас: перенаправляет на другой существующий токенСужение публичного интерфейса к части большого сервиса

Современный стиль фабрики использует inject() внутри функции вместо старого массива deps. Внутри useFactory доступен контекст инжектора, поэтому inject() работает напрямую и зависимости остаются типизированными.

useExisting и useClass на один и тот же класс ведут себя по-разному. useExisting переиспользует уже созданный экземпляр (один объект на оба ключа), а useClass создаёт отдельный экземпляр для нового токена. Путаница тут приводит к двум копиям сервиса с раздельным состоянием.

Зависимость нужно собрать из двух других сервисов и значения конфигурации, доступного только в рантайме. Какой рецепт подходит?

Multi-провайдеры и конфигурационные токены

Обычный провайдер по ключу хранит ровно одно значение: последняя регистрация затирает предыдущие. Multi-провайдер меняет правило. С флагом multi: true инжектор собирает все регистрации одного токена в массив, и inject() вернёт массив значений. Так в Angular устроены расширяемые точки, куда несколько частей приложения добавляют свой вклад.

Без multi: true вторая регистрация просто заменила бы первую. С флагом каждая запись добавляется в общий массив. Именно по этому принципу работает HTTP_INTERCEPTORS: каждый интерсептор регистрируется как multi, а HTTP-стек проходит по собранному массиву по порядку.

Конфигурационный токен - граница между кодом и окружением. Сервис зависит от APP_CONFIG, а не от глобальной переменной или импорта файла environment. Это делает сервис чистым для тестов: значение подменяется одной строкой в providers, а сам сервис остаётся нетронутым.

Две части приложения регистрируют один и тот же токен VALIDATORS без флага multi. Что вернёт inject(VALIDATORS)?

Связь с другими темами

Урок про ключи и рецепты DI. Дальше курс показывает, где эти провайдеры живут и как их применяют:

  • Иерархия инжекторов — Тот же провайдер можно положить в корень или в конкретный компонент, и результат разный
  • HttpClient — provideHttpClient и интерсепторы регистрируются именно как провайдеры и multi-токены

Итог

  • InjectionToken<T> - типобезопасный уникальный ключ для значений, у которых нет класса: конфигов, строк, интерфейсов
  • Пять рецептов: useClass подставляет другую реализацию, useValue даёт готовое значение, useFactory вычисляет зависимость функцией, useExisting создаёт алиас на уже существующий провайдер
  • Multi-провайдеры (multi: true) собирают несколько регистраций одного токена в массив - так устроены HTTP_INTERCEPTORS
  • factory в самом InjectionToken задаёт значение по умолчанию и делает токен tree-shakable, если его никто не переопределяет
  • Конфигурационные токены отделяют код от окружения и позволяют подменять значения в тестах без правки сервисов

Связанные уроки

  • ng-20-hierarchical-di — Поняв рецепты провайдеров, разработчик переходит к тому, на каком уровне дерева инжекторов их размещать
  • ng-17-di-intro — Базовый inject() и провайдинг сервисов - фундамент, на котором строятся токены и рецепты
InjectionToken и провайдеры

0

1

Войти