angular
March 12, 2024

Иерархия инжекторов в Angular. Часть 1

Инжекторы в Angular имеют правила, которыми можно воспользоваться, чтобы достичь желаемой видимости провайдеров в приложениях. Понимая эти правила, можно сделать правильный выбор: объявлять ли провайдер на уровне приложения или компонента

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

Могут быть секции страницы, работающие независимо от остальной части приложения, с собственными локальными копиями необходимых сервисов и других зависимостей. Некоторые сервисы могут использоваться совместно с другими частями приложения или с родительскими компонентами, находящимися выше в дереве компонентов, в то время как другие зависимости предназначены для частного использования.

Иерархическая инъекция зависимостей позволяет изолировать секции приложения, предоставляя им собственные частные зависимости, не используемые остальной частью приложения, или позволяют родительским компонентам делиться определенными зависимостями только со своими дочерними компонентами, но не с остальной частью дерева компонентов.

Иерархическая инъекция зависимостей обеспечивает возможность совместного использования зависимостей между различными частями приложения только тогда, когда это необходимо.

Иерархия инжекторов

В Angular существуют правила для инжекторов. Понимая их, можно определить, где объявлять провайдер - на уровне приложения, в компоненте или в директиве.

В Angular есть три иерархии инжекторов:

  1. Environment Injector
  2. Module Injector
  3. Element Injector

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()

Компонент является особым типом директивы, что означает, что как и у @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 🌷.