Angular позволяет создавать компоненты, каналы, директивы и многое другое. Для настройки необходимых зависимостей Angular предоставляет технологию под названием Dependency Injection (внедрение зависимостей).
Есть два механизма — провайдер зависимостей (dependency provider) и потребитель зависимостей (dependency consumer). Взаимодействие между ними возможно благодаря абстрактному объекту, называемому инжектором (injector).
Рассмотрим пример.
Добавление декоратора Injectable
к классу показывает, что он доступен для внедрения и может быть внедрен.
@Injectable()
class OneService {}
Теперь OneService
действует как зависимость, которая может быть внедрена в несколько мест.
Где она может быть внедрена?
- На уровне компонента.
- На уровне модуля приложения.
P. S. Рассмотрим уровень модуля отдельно (в условиях отложенной загрузки).
На уровне компонента можно просто добавить провайдеры в декоратор компонента:
@Component({
selector: 'app-list',
template: '...',
providers: [oneService]
})
class TestComponent {}
На уровне модуля аналогично предоставляем зависимость в провайдеры в декораторе NgModule
:
@NgModule({
selector: 'app-list',
template: '...',
providers: [oneService]
})
class TestComponent {}
Из компонента также можно непосредственно добавить зависимость на корневом уровне, т.е. с помощью встроенного модуля, используя providedIn:'root'
.
На данный момент это наиболее часто используемый вариант:
@Injectable({
providedIn: 'root'
})
class TestService {}
После добавления зависимости в провайдеры можно использовать ее и внедрять в элементы там, где это требуется.
Самый распространенный способ внедрения зависимости — добавление ее в конструктор соответствующего компонента, где нужно использовать эту зависимость.
constructor(private loggerService:LoggerService) { }
Ранее я упомянул об абстрактном объекте — инжекторе, управляющим взаимодействием между потребителями и провайдерами.
Посмотрим, как это происходит.
Повторное использование экземпляра
Мы добавили зависимость в конструктор, чтобы использовать ее в компоненте. Поэтому, когда компонент инстанцируется, Angular проверяет, существует ли уже экземпляр запрашиваемой зависимости. Если он существует, то Angular продолжает работу с ним. Если нет, инжектор создаст необходимый экземпляр.
P. S. Для проверки наличия экземпляров используется восходящий подход к разрешению, который будет продемонстрирован ниже.
Экземпляр относительно определенного уровня
Обсудим еще один момент, связанный с экземпляром. Рассмотрим пример, когда зависимость внедряется на уровне компонента.
Предположим, у нас два компонента, и оба они требуют одну и ту же зависимость, которую мы обеспечили на уровне каждого компонента.
Соответственно у этих двух компонентов будут созданы разные экземпляры.
А когда мы предоставляем зависимость на корневом уровне, т. е. на уровне модуля приложения, экземпляр, созданный для зависимости, остается неизменным во всем приложении.
Теперь следует ответить на вопрос: почему мы определяем экземпляр в модуле приложения, а не в компоненте приложения?
Дело в том, что в соответствии с иерархией модулей Angular модуль приложения находится выше, чем компонент приложения.
Примечание: над модулем приложения (App Module) находится модульный инжектор (Module injector), настраиваемый модулем платформы (Platform Module), а еще выше — нулевой инжектор (Null Injector), на котором все заканчивается. За более подробной информацией можно обратиться к официальной документации.
Область применения провайдера
Как ограничить область применения провайдера?
В модуле с динамической загрузкой инжектор в корне делает все упомянутые провайдеры доступными в начале работы приложения.
Теперь предположим, что на определенном маршруте мы отложено загружаем модуль, и он требует соответствующие зависимости, которые указаны в его массиве провайдеров, т.е. провайдеров для конкретного модуля. Но ведь корневой компонент не знает об этом.
Поэтому в отношении отложено загружаемого модуля дочерний инжектор заполняется в соответствии с корневым инжектором и полностью удовлетворяет требования сервисов, указанных в массиве провайдеров этого модуля.
Созданный отложено загруженный модуль имеет свой экземпляр.
Он может быть реализован следующим образом:
import { Injectable } from ‘@angular/core’; @Injectable({ providedIn: ‘any’, }) export class SomeService {}
Помните, мы обсуждали внедрение сервисов на уровне компонента? Это называется ограничением области применения провайдеров на уровне компонента.
Теперь вы можете спросить: если я внедрил одну и ту же зависимость (сервис) на уровне модуля приложения и на уровне компонента, то как Angular узнает, какой экземпляр использовать?
Дело в том, что есть набор правил разрешения, которым следует Angular во избежание любого типа конфликта.
Разрешение происходит в два этапа.
1. ModuleInjector. Он может быть настроен одним из двух способов с помощью:
- свойства
@Injectable() providedIn
; - массива провайдеров
@NgModule()
.
2. ElementInjector. Предоставление сервиса в декораторе @Component()
с помощью провайдеров или свойства viewProviders
настраивает ElementInjector
.
Когда компонент объявляет зависимость, Angular прежде всего пытается удовлетворить эту зависимость с помощью своего ElementInjector
. Если это не удается, то запрос передается вверх к ElementInjector
родительского компонента, пока Angular не найдет инжектор, способный обработать запрос, или не исчерпает иерархию родительских компонентов ElementInjector
.
Для разрешения запроса используется восходящий подход.
Если Angular не удалось найти провайдера ни в одной иерархии ElementInjector
, т.е. в провайдерах компонентного уровня, он возвращается к элементу, в котором возник запрос, и проводит поиск в иерархии ModuleInjector
. Если и после этого провайдер не будет найден, Angular выдаст ошибку.
Читайте также:
- Избавляемся от рендеринга в Angular: только функциональность и никакого рендера
- Платформы Аngular в деталях. Часть 3. Визуализация Angular-приложений в терминале
- Protractor мертв, да здравствует Cypress!
Читайте нас в Telegram, VK и Дзен
Перевод статьи Vikas Tiwari: Dependency Injection-Behind the Scenes