Angular

Dependency Injection и inject()

Компонент списка задач хочет загрузить данные с сервера. Он мог бы сам создать HttpClient через new, настроить заголовки, базовый URL, обработку ошибок. Но тогда каждый компонент копировал бы эту настройку, а в тестах подменить сеть было бы невозможно. Dependency Injection переворачивает подход: компонент лишь объявляет, что ему нужен HttpClient, а Angular сам находит готовый экземпляр и передаёт его. В Angular 21 это объявление чаще всего пишут через функцию inject - короткую и гибкую замену внедрению через конструктор.

  • HttpClient: компоненты и сервисы получают единый настроенный клиент вместо собственных new
  • Сервисы состояния: общий сервис корзины внедряется в десятки компонентов как один экземпляр
  • Тестирование: в тестах реальный сервис подменяется заглушкой через провайдеры, код компонента не меняется
  • Маршрутизатор и ActivatedRoute: внедряются в компоненты для навигации и чтения параметров
  • Токены конфигурации: базовый URL API или флаги функций раздаются через InjectionToken

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

  • Понимание компонентов и сервисов как классов Angular
  • Базовое знание классов TypeScript и их конструкторов
  • Идея о зависимости: один класс использует другой для своей работы

DI как опора Angular с самого начала

Внедрение зависимостей было центральной идеей Angular ещё с первой версии AngularJS, а во второй версии (2016) превратилось в строгую систему на основе классов и провайдеров. Идея пришла из принципа инверсии управления, известного в серверной разработке (Spring, .NET). Долгие годы зависимости объявляли только через параметры конструктора с декораторами. В версии 14 (2022) Angular добавил функцию inject, дающую тот же результат вне конструктора, что оказалось удобнее с приходом standalone-компонентов и сигналов. К Angular 21, где standalone и сигналы стали стандартом, inject стал предпочтительным способом получать зависимости.

Что такое внедрение зависимостей

Зависимость - это объект, который нужен классу для работы: например, компоненту нужен HttpClient для запросов. Без DI класс создавал бы зависимость сам через new, жёстко привязываясь к конкретной реализации и её настройке. Внедрение зависимостей разрывает эту связь: класс лишь объявляет, что ему нужно, а ответственность за создание и передачу берёт на себя система DI. Это применение принципа инверсии управления - объект не управляет своими зависимостями, ими управляют извне.

  • Без DI — Класс сам создаёт зависимости через new. Жёсткая привязка к реализации, дублирование настройки, невозможность подмены в тестах
  • С DI — Класс объявляет потребность, система создаёт и передаёт экземпляр. Слабая связанность, единая настройка, лёгкая подмена в тестах

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

DI - это не магия Angular, а распространённый архитектурный паттерн. Те же идеи лежат в основе Spring в Java и встроенного контейнера в .NET. Angular лишь встроил DI в ядро так, что им пользуются повсеместно, от HttpClient до пользовательских сервисов.

В чём суть внедрения зависимостей?

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

Чтобы система знала, как создать зависимость, её регистрируют через провайдер. Самый частый способ для сервиса - указать providedIn: 'root' в декораторе Injectable: тогда сервис доступен во всём приложении как единый экземпляр. Хранением и выдачей экземпляров занимается инжектор. Инжекторов несколько, и они образуют иерархию: корневой инжектор приложения, а под ним инжекторы отдельных компонентов.

Когда класс запрашивает зависимость, Angular ищет провайдер начиная с ближайшего инжектора и поднимается по иерархии вверх до корня. Если провайдер найден в инжекторе компонента - используется он, и его экземпляр виден только этому поддереву. Если нет - поиск идёт выше, вплоть до корневого инжектора. Это и есть иерархический разрешитель зависимостей.

Уровень регистрации определяет область видимости и время жизни. providedIn: 'root' даёт один экземпляр на всё приложение. Провайдер в компоненте создаёт отдельный экземпляр для каждого экземпляра этого компонента и его потомков.

Как Angular находит провайдер при запросе зависимости?

inject() против конструктора и контекст инъекции

Получить зависимость можно двумя способами. Классический - объявить её параметром конструктора, и Angular подставит экземпляр при создании класса. Современный - вызвать функцию inject с токеном зависимости (обычно классом сервиса) прямо в инициализаторе поля. Результат тот же, но inject короче и гибче, особенно при наследовании и вынесении общей логики в функции.

Преимущество inject заметно при переиспользовании логики. Общий код, использующий несколько зависимостей, можно вынести в обычную функцию, которая внутри вызывает inject, и применять её в разных компонентах. С конструктором так не выйдет: зависимости пришлось бы пробрасывать через параметры в каждом классе. Это пример того, как inject снижает дублирование и связанность.

inject работает только в контексте инъекции. Это инициализатор поля класса, тело конструктора и фабрика провайдера. Вызов inject внутри обычного метода или обработчика события вне этого контекста приведёт к ошибке. Если зависимость нужна позже, её получают в поле и используют через this.

В новом коде на Angular 21 по умолчанию выбирают inject: он короче, лучше работает с наследованием и позволяет выносить логику с зависимостями в переиспользуемые функции. Внедрение через конструктор остаётся валидным, особенно в существующем коде.

Где можно вызывать функцию inject?

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

DI - фундамент, на котором держатся сервисы и общее состояние приложения:

  • Сервисы и состояние на сигналах — Сервисы регистрируются как провайдеры и внедряются через DI в компоненты
  • effect — effect создаётся в контексте инъекции, который вводит этот урок
  • RxJS-интероп — toSignal и toObservable тоже требуют контекста инъекции для работы

Итог

  • Dependency Injection - это система, в которой класс объявляет нужные зависимости, а Angular сам их создаёт и передаёт
  • Провайдер описывает, как создать зависимость, а инжектор хранит и выдаёт экземпляры по запросу
  • Инжекторы образуют иерархию, и поиск зависимости идёт вверх от компонента к корню приложения
  • Функция inject получает зависимость без параметра конструктора и гибче в наследовании и переиспользовании логики
  • Контекст инъекции - это места (конструктор, инициализатор поля, фабрика провайдера), где inject доступен

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

  • ng-18-services — Сервисы создаются и внедряются через DI, поэтому это прямое продолжение темы
  • ng-13-effect — effect создаётся в контексте инъекции, понятие которого вводит этот урок
  • ng-16-rxjs-interop — toSignal и toObservable тоже работают в контексте инъекции
Dependency Injection и inject()

0

1

Войти