Иерархия инжекторов в Angular. Часть 1
Инжекторы в Angular имеют правила, которыми можно воспользоваться, чтобы достичь желаемой видимости провайдеров в приложениях. Понимая эти правила, можно сделать правильный выбор: объявлять ли провайдер на уровне приложения или компонента
Приложения на Angular могут быть достаточно объемными, и один из способов управления этой сложностью - разделение приложения на четко определенное дерево компонентов.
Могут быть секции страницы, работающие независимо от остальной части приложения, с собственными локальными копиями необходимых сервисов и других зависимостей. Некоторые сервисы могут использоваться совместно с другими частями приложения или с родительскими компонентами, находящимися выше в дереве компонентов, в то время как другие зависимости предназначены для частного использования.
Иерархическая инъекция зависимостей позволяет изолировать секции приложения, предоставляя им собственные частные зависимости, не используемые остальной частью приложения, или позволяют родительским компонентам делиться определенными зависимостями только со своими дочерними компонентами, но не с остальной частью дерева компонентов.
Иерархическая инъекция зависимостей обеспечивает возможность совместного использования зависимостей между различными частями приложения только тогда, когда это необходимо.
Иерархия инжекторов
В Angular существуют правила для инжекторов. Понимая их, можно определить, где объявлять провайдер - на уровне приложения, в компоненте или в директиве.
В Angular есть три иерархии инжекторов:
Environment Injector
EnvironmentInjector
может быть настроен одним из двух способов, используя:
- Свойство
providedIn
в декораторе@Injectable()
, чтобы указатьroot
илиplatform
- Массив
providers
вApplicationConfig
Tree-shaking and @Injectable()
Использование свойства providedIn
в декораторе @Injectable()
предпочтительнее, чем использование массива providers
в ApplicationConfig
. С providedIn
в @Injectable()
, инструменты оптимизации могут выполнять tree-shaking, что позволяет удалять сервисы, не используемые в приложении, что приводит к уменьшению размера пакета.
Tree-shaking особенно полезен для библиотек, так как приложение, использующее библиотеку, может не нуждаться в её инъекции. Подробнее о tree-shakable провайдерах читайте во введении к сервисам и внедрению зависимостей.
EnvironmentInjector
настраивается с помощью ApplicationConfig.providers
.
Провайдер подключается с помощью свойства providedIn
декоратора @Injectable()
следующим образом:
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' // <--provides this service in the root ElementInjector }) export class ItemService { name = 'telephone'; }
Декоратор @Injectable(
) идентифицирует класс сервиса. Свойство providedIn
настраивает конкретный EnvironmentInjector
, здесь root
, что делает сервис доступным в корневом EnvironmentInjector
.
Module Injector
В случае приложений на основе NgModule
можно настроить ModuleInjector
одним из двух способов, используя:
- Свойство
providedIn
декоратора@Injectable()
для ссылки наroot
илиplatform
- Массив
providers
декоратора@NgModule()
ModuleInjector
настраивается с помощью свойств @NgModule.providers
и @NgModule.imports
. ModuleInjector
представляет собой объединение всех массивов providers
, к которым можно получить доступ, следуя рекурсивно по NgModule.imports
.
Подчиненные иерархии ModuleInjector
создаются при ленивой загрузке других @NgModule
.
Platform Injector
Над root
существует еще два инжектора: дополнительный EnvironmentInjector
и NullInjector()
.
Рассмотрим, как Angular инициализирует приложение с помощью следующего кода в main.ts
:
bootstrapApplication(AppComponent, appConfig);
Метод bootstrapApplication()
создает дочерний инжектор инжектора платформы, который настраивается с помощью экземпляра ApplicationConfig
. Это корневой EnvironmentInjector
.
Метод platformBrowserDynamic()
создает инжектор, настроенный с помощью PlatformModule
, который содержит зависимости, специфичные для платформы. Это позволяет нескольким приложениям использовать одну конфигурацию платформы. Например, в браузере есть только одна строка URL-адреса, независимо от того, сколько приложений вы запускаете. Вы можете настроить дополнительные специфичные для платформы провайдеры на уровне платформы, предоставив дополнительные провайдеры с помощью функции platformBrowser()
.
Следующий родительский инжектор в иерархии - это NullInjector()
, который является вершиной дерева. Если вы поднялись так высоко вверх по дереву, что ищете сервис в NullInjector()
, вы получите ошибку, если не использовали @Optional()
, потому что в конечном итоге все заканчивается на NullInjector()
, и он возвращает ошибку или, в случае @Optional()
, null. Для получения дополнительной информации о @Optional()
смотрите раздел @Optional()
в этом руководстве.
Следующая диаграмма представляет отношения между корневым ModuleInjector
и его родительскими инжекторами, как описано выше
Хотя имя root
является специальным псевдонимом, другие иерархии EnvironmentInjector
не имеют псевдонимов. У разработчика всегда есть возможность создавать иерархии EnvironmentInjector
, когда компонент динамически загружается, например, с помощью маршрутизатора, который создает дочерние иерархии EnvironmentInjector.
Все запросы перенаправляются к корневому инжектору, независимо от того, настроили ли вы его с помощью экземпляра ApplicationConfig
, переданного методу bootstrapApplication()
, или зарегистрировали все провайдеры с корнем в собственных службах.
@Injectable() vs. ApplicationConfig
При настройке провайдера для всего приложения в ApplicationConfig
методе bootstrapApplication
, он переопределяет провайдер, настроенный для root
в метаданных @Injectable()
. Это можно сделать для настройки нестандартного провайдера службы, который используется несколькими приложениями.
Вот пример ситуации, когда конфигурация маршрутизатора компонента включает нестандартную LocationStrategy
, добавив ее провайдер в список провайдеров ApplicationConfig
.
providers: [ { provide: LocationStrategy, useClass: HashLocationStrategy } ]
Для приложений, основанных на NgModule
, настраиваются провайдеры для всего приложения в AppModule.
ElementInjector
Angular
неявно создает иерархии ElementInjector
для каждого DOM-элемента.
Объявление сервиса в декораторе @Component()
с использованием свойств providers
или viewProviders
настраивает ElementInjector
. Например, следующий TestComponent
настраивает ElementInjector
, объявляя сервис следующим образом:
@Component({ … providers: [{ provide: ItemService, useValue: { name: 'lamp' } }] }) export class TestComponent
Когда сервисы подключаются в компоненте, то они доступны через ElementInjector
в этом экземпляре компонента. Он также может быть виден в дочерних компонентах/директивах в соответствии с правилами видимости, описанными в разделе правил разрешения.
Когда экземпляр компонента уничтожается, уничтожается также и экземпляр этого сервиса.
Компонент является особым типом директивы, что означает, что как и у @Directive()
, у @Component()
также есть свойство providers
. Когда разработчик настраивает провайдер для компонента или директивы с использованием свойства providers
, этот провайдер принадлежит ElementInjector
этого компонента или директивы.
Компоненты и директивы на одном элементе используют общий инжектор.
Фазы поиска инжекторов
При поиске токена для компонента Angular проходит две фазы:
1. ElementInjector.
2. EnvironmentInjector.
Первым шагом поиск происходит на уровне Element Injector. Если инжектор компонента не содержит провайдера, он передает запрос своему родительскому компоненту в ElementInjector
. Запросы продолжают перенаправляться вверх, пока Angular не найдет инжектор, который может обработать запрос, или пока не будет исчерпана иерархия родительских ElementInjector
.
Если Angular не находит провайдер в какой-либо из иерархий ElementInjector
, он возвращается к элементу, откуда начался запрос, и ищет в иерархии EnvironmentInjector
. Если Angular все еще не находит провайдер, он генерирует ошибку.
Если было зарегистрировано несколько провайдеров на разных уровнях, то Angular использует тот, который он встречает первым. Если, например, провайдер зарегистрирован локально в компоненте, который нуждается в сервисе, Angular не ищет другой провайдер того же сервиса.
Если мы работаем с приложением, которое основано на NgModule, то после ElementInjector поиск будет осуществлён в ModuleInjector
Модификаторы зависимостей
Поведение при поиске провайдеров можно модифицировать с помощью @Optional()
, @Self()
, @SkipSelf()
и @Host()
. Импортируйте каждый из них из @angular/core
и используйте в конструкторе класса компонента или при конфигурации инъекции вашего сервиса.
Для рабочего приложения, демонстрирующего модификаторы разрешения, описанные в этом разделе, смотрите пример модификаторов разрешения / загрузите пример.
Модификаторы разрешения делятся на три категории:
@Optional()
позволит продолжить работу, если зависимость не будет найдена.@SkipSelf()
пропустит поиск в текущем ElementInjector.@Host()
и@Self()
осуществят поиск только в текущем ElementInjector или его хосте.
По умолчанию Angular всегда начинает с текущего инжектора и продолжает поиск вверх по иерархии. Модификаторы позволяют изменить начальное или текущее местоположение и конечное местоположение.
Кроме того, вы можете комбинировать все модификаторы, за исключением: @Host() и @Self(), а так же @SkipSelf() и @Self().
Пример по ссылке
@Optional
@Optional()
в Angular позволяет считать сервис, который вы инжектируете, необязательным. Таким образом, если его не получается найти во время выполнения, Angular вернет взамен null
, вместо того чтобы генерировать ошибку. В следующем примере сервис OptionalService
отсутствует в службе ApplicationConfig,
в @NgModule()
или в классе компонента, поэтому он недоступен нигде в приложении.
// src/app/optional/optional.component.ts export class OptionalComponent { constructor(@Optional() public optional?: OptionalService) {} }
@Self
@Self() сообщает Angular чтобы сервис искался только в ElementInjector
текущего компонента или директивы.
Хорошим примером использования @Self()
является инъекция сервиса, но только если он доступен на текущем хост-элементе. Чтобы избежать ошибок в этой ситуации, сочетайте @Self()
с @Optional()
.
// src/app/self-no-data/self-no-data.component.ts @Component({ standalone: true, selector: 'app-self-no-data', templateUrl: './self-no-data.component.html', styleUrls: ['./self-no-data.component.css'] }) export class SelfNoDataComponent { constructor(@Self() @Optional() public leaf?: LeafService) { } }
В этом примере есть родительский провайдер, и если бы мы не использовали @Self
и @Optional
, то инжектор вернул бы его значение. В нашем случае инжектор вернет null
, потому что @Self()
говорит инжектору искать только в текущем хост-элементе.
В другом примере показан класс компонента с провайдером для FlowerService
. В этом случае инжектор не ищет дальше текущего ElementInjector
, потому что находит FlowerService
и возвращает tulip 🌷
.
@Component({ standalone: true, selector: 'app-self', templateUrl: './self.component.html', styleUrls: ['./self.component.css'], providers: [{ provide: FlowerService, useValue: { emoji: '🌷' } }] }) export class SelfComponent { constructor(@Self() public flower: FlowerService) {} }
@SkipSelf
@SkipSelf()
- это антагонист @Self()
. С помощью @SkipSelf()
Angular начинает поиск сервиса в родительском ElementInjector
, а не в текущем. Так если родительский ElementInjector
использовал значение эмодзи для fern🌿
, но у вас был leaf 🍁
в массиве провайдеров компонента, Angular проигнорировал бы leaf 🍁
и использовал бы fern 🌿
.
Чтобы увидеть это в коде, представим, что следующее значение для эмодзи - это то, что использовал бы родительский компонент, как в этом сервисе:
xport class LeafService { emoji = '🌿'; }
Представим, что в дочернем компоненте было бы другое значение, leaf 🍁
, но вы хотели бы использовать значение из родительского компонента. Вот когда вы бы использовали @SkipSelf()
:
@Component({ standalone: true, selector: 'app-skipself', templateUrl: './skipself.component.html', styleUrls: ['./skipself.component.css'], // Angular would ignore this LeafService instance providers: [{ provide: LeafService, useValue: { emoji: '🍁' } }] }) export class SkipselfComponent { // Use @SkipSelf() in the constructor constructor(@SkipSelf() public leaf: LeafService) { } }
В этом случае сервис вернет fern🌿
, потому что текущий хост-элемент будет пропущен.
@SkipSelf() вместе с @Optional()
@SkipSelf()
используется вместе с @Optional()
, чтобы избежать ошибки, если значение равно null
. В следующем примере сервис Person
внедряется в конструктор. @SkipSelf()
указывает Angular пропустить текущий инжектор, и @Optional()
предотвратит ошибку, если сервис Person
будет равен null
.
class Person { constructor(@Optional() @SkipSelf() parent?: Person) {} }
@Host
@Host()
позволяет указать компонент как последнюю точку в дереве инжекторов при поиске провайдеров. Даже если далее в дереве присутствует экземпляр сервиса, Angular не будет продолжать поиск. @Host()
можно использовать следующим образом:
@Component({ standalone: true, selector: 'app-host', templateUrl: './host.component.html', styleUrls: ['./host.component.css'], // provide the service providers: [{ provide: FlowerService, useValue: { emoji: '🌷' } }], imports: [HostChildComponent] }) export class HostComponent { // use @Host() in the constructor when injecting the service constructor(@Host() @Optional() public flower?: FlowerService) { } }
Поскольку в HostComponent
используется @Host()
в конструкторе, несмотря на то, что у родителя HostComponent
может быть значение flower.emoji
, HostComponent
будет использовать tulip 🌷
.