Spec-Zone.ru › Angular

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

В этом руководстве подробно рассматривается система иерархического внедрения зависимостей Angular, включая правила разрешения зависимостей, модификаторы и расширенные шаблоны.

ПРИМЕЧАНИЕ: Основные понятия об иерархии инжекторов и области действия провайдеров см. в руководстве по объявлению провайдеров зависимостей.

Типы иерархий инжекторов

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

Иерархии инжекторов Подробности
EnvironmentInjector hierarchy Настройте EnvironmentInjector в этой иерархии с помощью @Service() или массива providers в ApplicationConfig.
иерархия ElementInjector Неявно создаётся для каждого DOM-элемента. По умолчанию ElementInjector пуст, если вы не настроили его в свойстве providers у @Directive() или @Component().

Приложения на основе NgModule

В приложениях на основе NgModule зависимости можно предоставлять с помощью иерархии ModuleInjector, используя @NgModule() или аннотацию @Injectable().

EnvironmentInjector

EnvironmentInjector можно настроить одним из двух способов с помощью:

  • @Service()
  • массива providers в ApplicationConfig

Устранение неиспользуемого кода и @Service()

Предпочтительнее использовать декоратор @Service(), а не массив providers в ApplicationConfig. С помощью @Service инструменты оптимизации могут выполнять устранение неиспользуемого кода, удаляя сервисы, которые приложение не использует. В результате размер сборки уменьшается.

Устранение неиспользуемого кода особенно полезно для библиотеки, поскольку приложению, использующему библиотеку, может не понадобиться внедрять её.

EnvironmentInjector настраивается с помощью ApplicationConfig.providers.

Предоставляйте сервисы с помощью @Service() следующим образом:

import {Service} from '@angular/core';

@Service() // <--provides this service in the root EnvironmentInjector
export class ItemService {
  name = 'telephone';
}

Декораторы @Service() или @Injectable() указывают, что класс является сервисом.

ModuleInjector

В приложениях на основе NgModule ModuleInjector можно настроить одним из двух способов с помощью:

  • декоратора @Service(),
  • свойства providedIn у @Injectable(), ссылающегося на root или platform
  • массива providers в @NgModule()

ModuleInjector настраивается с помощью свойств @NgModule.providers и NgModule.imports. ModuleInjector представляет собой объединение всех массивов провайдеров, доступных при рекурсивном обходе NgModule.imports.

Дочерние иерархии ModuleInjector создаются при ленивой загрузке других @NgModules.

Инжектор платформы

Над root есть ещё два инжектора: дополнительный EnvironmentInjector и NullInjector().

Рассмотрим, как Angular запускает приложение с помощью следующего кода в main.ts:

bootstrapApplication(App, appConfig);

Метод bootstrapApplication() создаёт дочерний инжектор инжектора платформы, настроенный экземпляром ApplicationConfig. Это корневой root EnvironmentInjector.

Метод platformBrowserDynamic() создаёт инжектор, настроенный с помощью PlatformModule, содержащего зависимости, специфичные для платформы. Это позволяет нескольким приложениям совместно использовать конфигурацию платформы. Например, в браузере есть только одна адресная строка, независимо от количества запущенных приложений. Вы можете настроить дополнительные провайдеры для платформы, передав extraProviders в функцию platformBrowser().

Следующий родительский инжектор в иерархии — NullInjector(), который находится на вершине дерева. Если вы поднялись настолько высоко по дереву, что ищете сервис в NullInjector(), произойдёт ошибка, если вы не использовали @Optional(), поскольку в конечном итоге всё заканчивается на NullInjector(), который возвращает ошибку или, в случае @Optional(), null. Дополнительные сведения о @Optional() см. в разделе @Optional() этого руководства.

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

EnvironmentInjector
(настраивается Angular)
содержит специальные компоненты, например DomSanitizer => providedIn 'platform'

корневой EnvironmentInjector
(настраивается с помощью AppConfig)
содержит компоненты приложения => bootstrapApplication(..., AppConfig)

NullInjector
всегда выдаёт ошибку, если
не использовать @Optional()

Хотя имя root является специальным псевдонимом, у других иерархий EnvironmentInjector псевдонимов нет. Вы можете создавать иерархии EnvironmentInjector при динамическом создании компонентов, например с помощью Router, который создаёт дочерние иерархии EnvironmentInjector.

Все запросы передаются корневому инжектору независимо от того, настроили ли вы его с помощью экземпляра ApplicationConfig, переданного методу bootstrapApplication(), или зарегистрировали все провайдеры с помощью root в самих сервисах.

@Injectable() и ApplicationConfig

Если вы настроите общеприложенческий провайдер в ApplicationConfig метода bootstrapApplication, он переопределит провайдер, настроенный для root в метаданных @Injectable(). Так можно настроить нестандартный провайдер сервиса, совместно используемого несколькими приложениями.

Вот пример случая, когда конфигурация маршрутизатора компонентов включает нестандартную стратегию расположения, добавляя её провайдер в список providers в ApplicationConfig.

providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}];

В приложениях на основе NgModule настройте общеприложенческие провайдеры в AppModule providers.

ElementInjector

Angular неявно создаёт иерархии ElementInjector для каждого DOM-элемента.

Предоставление сервиса в декораторе @Component() с помощью свойства providers или viewProviders настраивает ElementInjector. Например, следующий TestComponent настраивает ElementInjector, предоставляя сервис следующим образом:

@Component({
  /* … */
  providers: [{ provide: ItemService, useValue: { name: 'lamp' } }]
})
export class TestComponent

ПОЛЕЗНО: Чтобы понять связь между деревом EnvironmentInjector, ModuleInjector и деревом ElementInjector, см. раздел правила разрешения зависимостей.

Если вы предоставляете сервисы в компоненте, этот сервис доступен через ElementInjector в экземпляре компонента. Он также может быть виден дочерним компонентам и директивам согласно правилам видимости, описанным в разделе правила разрешения зависимостей.

При уничтожении экземпляра компонента уничтожается и экземпляр сервиса.

@Directive() и @Component()

Компонент — это особый тип директивы. Поэтому так же, как у @Directive() есть свойство providers, оно есть и у @Component(). Это означает, что как директивы, так и компоненты могут настраивать провайдеры с помощью свойства providers. Если вы настраиваете провайдер для компонента или директивы с помощью свойства providers, этот провайдер принадлежит ElementInjector данного компонента или директивы. Компоненты и директивы на одном элементе используют общий инжектор.

Правила разрешения зависимостей

При разрешении токена для компонента или директивы Angular выполняет поиск в два этапа:

  1. Среди родителей в иерархии ElementInjector.
  2. Среди родителей в иерархии EnvironmentInjector.

Когда компонент объявляет зависимость, Angular пытается удовлетворить её с помощью собственного ElementInjector. Если в инжекторе компонента нет нужного провайдера, запрос передаётся вверх, в ElementInjector родительского компонента.

Запросы продолжают передаваться вверх, пока Angular не найдёт инжектор, способный обработать запрос, или пока не закончатся родительские иерархии ElementInjector.

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

Если вы зарегистрировали провайдер для одного DI-токена на разных уровнях, Angular использует первый найденный провайдер для разрешения зависимости. Например, если провайдер зарегистрирован локально в компоненте, которому нужен сервис, Angular не будет искать другой провайдер того же сервиса.

ПОЛЕЗНО: В приложениях на основе NgModule Angular выполнит поиск в иерархии ModuleInjector, если не найдёт провайдер в иерархиях ElementInjector.

Модификаторы разрешения зависимостей

Поведение разрешения зависимостей Angular можно изменить с помощью optional, self, skipSelf и host. Импортируйте каждый из них из @angular/core и используйте в конфигурации inject при внедрении сервиса.

Типы модификаторов

Модификаторы разрешения зависимостей делятся на три категории:

  • Что делать, если Angular не находит искомый объект, то есть optional
  • С какого места начинать поиск, то есть skipSelf
  • Где прекращать поиск: host и self

По умолчанию Angular всегда начинает поиск в текущем Injector и продолжает его вверх по иерархии. Модификаторы позволяют изменить начальное, или собственное, местоположение и конечное местоположение поиска.

Кроме того, вы можете сочетать все модификаторы, кроме следующих комбинаций:

  • host и self
  • skipSelf и self.

optional

optional позволяет Angular считать внедряемый сервис необязательным. Если во время выполнения его не удаётся разрешить, Angular возвращает для сервиса значение null вместо того, чтобы выдавать ошибку. В следующем примере сервис OptionalService не предоставлен в сервисе, ApplicationConfig, @NgModule() или классе компонента, поэтому он недоступен нигде в приложении.

export class Optional {
  public optional? = inject(OptionalService, {optional: true});
}

self

Используйте self, чтобы Angular выполнял поиск только в ElementInjector текущего компонента или директивы.

Хороший пример использования self — внедрение сервиса только в том случае, если он доступен на текущем хост-элементе. Чтобы избежать ошибок в этой ситуации, сочетайте self с optional.

Например, в следующем SelfNoData обратите внимание на внедрённый LeafService в качестве свойства.

@Component({
  selector: 'app-self-no-data',
  templateUrl: './self-no-data.html',
  styleUrls: ['./self-no-data.css'],
})
export class SelfNoData {
  public leaf = inject(LeafService, {optional: true, self: true});
}

В этом примере есть родительский провайдер, и внедрение сервиса вернёт его значение. Однако внедрение сервиса с помощью self и optional вернёт null, поскольку self указывает инжектору прекратить поиск на текущем хост-элементе.

В другом примере у класса компонента есть провайдер для FlowerService. В этом случае инжектор не ищет дальше текущего ElementInjector, поскольку находит FlowerService и возвращает тюльпан 🌷.

@Component({
  selector: 'app-self',
  templateUrl: './self.html',
  styleUrls: ['./self.css'],
  providers: [{provide: FlowerService, useValue: {emoji: '🌷'}}],
})
export class Self {
  public flower = inject(FlowerService, {self: true});
}

skipSelf

skipSelf действует противоположно self. С skipSelf Angular начинает поиск сервиса в родительском ElementInjector, а не в текущем. Поэтому, если родительский ElementInjector использует значение fern 🌿 для emoji, а в массиве providers компонента указано значение maple leaf 🍁, Angular проигнорирует maple leaf 🍁 и использует fern 🌿.

Чтобы увидеть это в коде, предположим, что родительский компонент использует следующее значение emoji, как в этом сервисе:

export class LeafService {
  emoji = '🌿';
}

Представьте, что в дочернем компоненте у вас есть другое значение — кленовый лист 🍁, но вы хотите использовать значение родителя. В этом случае следует использовать skipSelf:

@Component({
  selector: 'app-skipself',
  templateUrl: './skipself.html',
  styleUrls: ['./skipself.css'],
  // Angular would ignore this LeafService instance
  providers: [{provide: LeafService, useValue: {emoji: '🍁'}}],
})
export class Skipself {
  // Use skipSelf as inject option
  public leaf = inject(LeafService, {skipSelf: true});
}

В этом случае для emoji вы получите fern 🌿, а не maple leaf 🍁.

Параметр skipSelf с optional

Используйте параметр skipSelf вместе с optional, чтобы избежать ошибки, если значение равно null.

В следующем примере сервис Person внедряется во время инициализации свойства. skipSelf указывает Angular пропустить текущий инжектор, а optional предотвращает ошибку, если сервис Person имеет значение null.

class Person {
  parent = inject(Person, {optional: true, skipSelf: true});
}

host

host позволяет назначить компонент последним уровнем дерева инжекторов при поиске провайдеров.

Даже если выше по дереву есть экземпляр сервиса, Angular не продолжит поиск. Используйте host следующим образом:

@Component({
  selector: 'app-host',
  templateUrl: './host.html',
  styleUrls: ['./host.css'],
  // provide the service
  providers: [{provide: FlowerService, useValue: {emoji: '🌷'}}],
})
export class Host {
  // use host when injecting the service
  flower = inject(FlowerService, {host: true, optional: true});
}

Поскольку у Host есть параметр host, независимо от того, какое значение flower.emoji есть у родителя Host, Host будет использовать tulip 🌷.

Модификаторы при внедрении через конструктор

Как было показано ранее, поведение внедрения через конструктор можно изменить с помощью @Optional(), @Self(), @SkipSelf() и @Host().

Импортируйте каждый из них из @angular/core и используйте в конструкторе класса компонента при внедрении сервиса.

export class SelfNoData {
  constructor(@Self() @Optional() public leaf?: LeafService) {}
}

Логическая структура шаблона

Если вы предоставляете сервисы в классе компонента, они видны в дереве ElementInjector в зависимости от того, где и как вы их предоставили.

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

Компоненты используются в шаблонах, как в следующем примере:

<app-root> <app-child />; </app-root>

ПОЛЕЗНО: Обычно компоненты и их шаблоны объявляются в отдельных файлах. Чтобы понять, как работает система внедрения зависимостей, полезно рассматривать их как единое логическое дерево. Термин логический отличает его от дерева отображения, то есть DOM-дерева приложения. Чтобы отметить расположение шаблонов компонентов, в этом руководстве используется псевдоэлемент <#VIEW>, которого в действительности нет в дереве отображения и который нужен только для наглядности.

Ниже показано, как деревья представлений <app-root> и <app-child> объединяются в одно логическое дерево:

<app-root>
  <#VIEW>
    <app-child>
     <#VIEW>
       …content goes here…
     </#VIEW>
    </app-child>
  </#VIEW>
</app-root>

Понимание назначения границы <#VIEW> особенно важно при настройке сервисов в классе компонента.

Пример: предоставление сервисов в @Component()

Способ предоставления сервисов с помощью декоратора @Component() (или @Directive()) определяет их видимость. В следующих разделах рассматриваются providers и viewProviders, а также способы изменения видимости сервисов с помощью skipSelf и host.

Класс компонента может предоставлять сервисы двумя способами:

Массивы Подробности
С помощью массива providers @Component({ providers: [SomeService] })
С помощью массива viewProviders @Component({ viewProviders: [SomeService] })

В примерах ниже показано логическое дерево приложения Angular. Чтобы проиллюстрировать работу инжектора в контексте шаблонов, логическое дерево будет представлять HTML-структуру приложения. Например, в логическом дереве будет показано, что <child-component> — непосредственный дочерний элемент <parent-component>.

В логическом дереве вы увидите специальные атрибуты: @Provide, @Inject и @ApplicationConfig. Это не настоящие атрибуты; они нужны, чтобы показать, что происходит «под капотом».

Атрибут сервиса Angular Подробности
@Inject(Token)=>Value Если в этой точке логического дерева внедрить Token, его значение будет Value.
@Provide(Token=Value) Указывает, что Token предоставляется с помощью Value в этой точке логического дерева.
@ApplicationConfig Показывает, что в этой точке следует использовать резервный EnvironmentInjector.

Структура приложения-примера

В примере приложения FlowerService предоставляется в root со значением emoji — красный гибискус 🌺.

@Service()
export class FlowerService {
  emoji = '🌺';
}

Рассмотрим приложение, состоящее только из App и Child. Самое простое отображение будет выглядеть как вложенные HTML-элементы, например:

<app-root>
  <!-- App selector -->
  <app-child> <!-- Child selector --> </app-child>
</app-root>

Однако при разрешении запросов на внедрение Angular использует логическое представление представлений, как показано ниже:

<app-root> <!-- App selector -->
  <#VIEW>
    <app-child> <!-- Child selector -->
      <#VIEW>
      </#VIEW>
    </app-child>
  </#VIEW>
</app-root>

Здесь <#VIEW> обозначает экземпляр шаблона. Обратите внимание, что у каждого компонента есть собственный <#VIEW>.

Знание этой структуры поможет вам предоставлять и внедрять сервисы, а также полностью контролировать их видимость.

Теперь рассмотрим случай, когда <app-root> внедряет FlowerService:

export class App {
  flower = inject(FlowerService);
}

Добавьте привязку в шаблон <app-root>, чтобы увидеть результат:

<p>Emoji from FlowerService: {{flower.emoji}}</p>

В представлении будет отображаться:

Emoji from FlowerService: 🌺

В логическом дереве это будет представлено следующим образом:

<app-root @ApplicationConfig
        @Inject(FlowerService) flower=>"🌺">
  <#VIEW>
    <p>Emoji from FlowerService: {{flower.emoji}} (🌺)</p>
    <app-child>
      <#VIEW>
      </#VIEW>
    </app-child>
  </#VIEW>
</app-root>

Когда <app-root> запрашивает FlowerService, инжектор должен разрешить токен FlowerService. Разрешение токена выполняется в два этапа:

  1. Инжектор определяет начальное и конечное местоположения поиска в логическом дереве. Начиная с начального местоположения, инжектор ищет токен на каждом уровне представления в логическом дереве. Если токен найден, он возвращается.

  2. Если токен не найден, инжектор ищет ближайший родительский EnvironmentInjector, которому можно передать запрос.

В этом примере условия следующие:

  1. Начать с <#VIEW>, принадлежащего <app-root>, и завершить поиск на <app-root>.

    • Обычно поиск начинается в точке внедрения. Однако в этом случае <app-root> — компонент. Компоненты @Component особенные тем, что у них также есть собственный viewProviders, поэтому поиск начинается с <#VIEW>, принадлежащего <app-root>. Для директивы, сопоставленной в той же точке, это было бы не так.
    • Конечная точка совпадает с самим компонентом, поскольку это верхний компонент в данном приложении.
  2. EnvironmentInjector, предоставленный через ApplicationConfig, служит резервным инжектором, если токен внедрения не найден в иерархиях ElementInjector.

Использование массива providers

Теперь добавьте в класс Child провайдер для FlowerService, чтобы в следующих разделах рассмотреть более сложные правила разрешения зависимостей:

@Component({
  selector: 'app-child',
  templateUrl: './child.html',
  styleUrls: ['./child.css'],
  // use the providers array to provide a service
  providers: [{provide: FlowerService, useValue: {emoji: '🌻'}}],
})
export class Child {
  // inject the service
  flower = inject(FlowerService);
}

Теперь, когда FlowerService предоставлен в декораторе @Component(), при запросе сервиса компонентом <app-child> инжектору достаточно выполнить поиск до ElementInjector в <app-child>. Продолжать поиск дальше по дереву инжекторов не потребуется.

Следующий шаг — добавить привязку в шаблон Child.

<p>Emoji from FlowerService: {{flower.emoji}}</p>

Чтобы отобразить новые значения, добавьте <app-child> в конец шаблона App, чтобы в представлении появился и подсолнух:

Child Component
Emoji from FlowerService: 🌻

В логическом дереве это выглядит следующим образом:

<app-root @ApplicationConfig
          @Inject(FlowerService) flower=>"🌺">
  <#VIEW>

  <p>Emoji from FlowerService: {{flower.emoji}} (🌺)</p>
  <app-child @Provide(FlowerService="🌻" )
             @Inject(FlowerService)=>"🌻"> <!-- search ends here -->
    <#VIEW> <!-- search starts here -->
    <h2>Child Component</h2>
    <p>Emoji from FlowerService: {{flower.emoji}} (🌻)</p>
  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

Когда <app-child> запрашивает FlowerService, инжектор начинает поиск в <#VIEW>, принадлежащем <app-child> (<#VIEW> включён, поскольку внедряется из @Component()), и завершает его на <app-child>. В данном случае FlowerService разрешается в массиве providers со значением sunflower 🌻 компонента <app-child>. Инжектору не нужно продолжать поиск по дереву. Он останавливается, как только находит FlowerService, и никогда не видит значение red hibiscus 🌺.

Использование массива viewProviders

Используйте массив viewProviders как ещё один способ предоставления сервисов в декораторе @Component(). При использовании viewProviders сервисы видны в <#VIEW>.

ПОЛЕЗНО: Действия такие же, как при использовании массива providers, за исключением того, что вместо него используется массив viewProviders.

Пошаговые инструкции приведены в этом разделе. Если вы можете настроить всё самостоятельно, перейдите к разделу Изменение доступности сервиса.

Для демонстрации мы создадим AnimalService, чтобы показать viewProviders. Сначала создайте AnimalService со свойством emoji, которому присвойте значение whale 🐳:

import {Service} from '@angular/core';

@Service()
export class AnimalService {
  emoji = '🐳';
}

Следуя той же схеме, что и для FlowerService, внедрите AnimalService в класс App:

export class App {
  public flower = inject(FlowerService);
  public animal = inject(AnimalService);
}

ПОЛЕЗНО: Весь код, относящийся к FlowerService, можно оставить: это позволит сравнить его с AnimalService.

Также добавьте массив viewProviders и внедрите AnimalService в класс <app-child>, но задайте emoji другое значение. Здесь ему присвоено значение dog 🐶.

@Component({
  selector: 'app-child',
  templateUrl: './child.html',
  styleUrls: ['./child.css'],
  // provide services
  providers: [{provide: FlowerService, useValue: {emoji: '🌻'}}],
  viewProviders: [{provide: AnimalService, useValue: {emoji: '🐶'}}],
})
export class Child {
  // inject services
  flower = inject(FlowerService);
  animal = inject(AnimalService);
}

Добавьте привязки в шаблоны Child и App. В шаблон Child добавьте следующую привязку:

<p>Emoji from AnimalService: {{animal.emoji}}</p>

Кроме того, добавьте такую же привязку в шаблон App:

<p>Emoji from AnimalService: {{animal.emoji}}</p>

Теперь в браузере должны отображаться оба значения:

App
Emoji from AnimalService: 🐳

Child Component
Emoji from AnimalService: 🐶

Логическое дерево для этого примера viewProviders выглядит так:

<app-root @ApplicationConfig
          @Inject(AnimalService) animal=>"🐳">
  <#VIEW>
  <app-child>
    <#VIEW @Provide(AnimalService="🐶")
    @Inject(AnimalService=>"🐶")>

    <!-- ^^using viewProviders means AnimalService is available in <#VIEW>-->
    <p>Emoji from AnimalService: {{animal.emoji}} (🐶)</p>
  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

Как и в примере с FlowerService, AnimalService предоставляется в декораторе <app-child> @Component(). Это означает, что, поскольку инжектор сначала ищет в ElementInjector компонента, он находит значение dog 🐶 для AnimalService. Ему не нужно продолжать поиск по дереву ElementInjector или искать в ModuleInjector.

providers и viewProviders

Поле viewProviders концептуально похоже на providers, но есть одно важное отличие. Провайдеры в viewProviders видны только внутри собственного представления компонента — содержимое, проецируемое в компонент через <ng-content>, не может получить к ним доступ.

Чтобы увидеть разницу между использованием providers и viewProviders, добавьте в пример ещё один компонент и назовите его Inspector. Inspector будет дочерним компонентом Child. В inspector.ts внедрите FlowerService и AnimalService при инициализации свойств:

export class Inspector {
  flower = inject(FlowerService);
  animal = inject(AnimalService);
}

Массив providers или viewProviders не нужен. Далее добавьте в inspector.html ту же разметку, что и в предыдущих компонентах:

<p>Emoji from FlowerService: {{flower.emoji}}</p>
<p>Emoji from AnimalService: {{animal.emoji}}</p>

Не забудьте добавить Inspector в массив imports компонента Child.

@Component({
  ...
  imports: [Inspector]
})

Затем добавьте следующее в child.html:

...

<div class="container">
  <h3>Content projection</h3>
  <ng-content />
</div>
<h3>Inside the view</h3>

<app-inspector />

<ng-content> позволяет проецировать содержимое, а <app-inspector> внутри шаблона Child делает Inspector дочерним компонентом Child.

Затем добавьте следующее в app.html, чтобы использовать проецирование содержимого.

<app-child>
  <app-inspector />
</app-child>

Теперь браузер отображает следующее; для краткости предыдущие примеры опущены:

...
Content projection

Emoji from FlowerService: 🌻
Emoji from AnimalService: 🐳

Emoji from FlowerService: 🌻
Emoji from AnimalService: 🐶

Эти четыре привязки показывают разницу между providers и viewProviders. Помните, что эмодзи собаки 🐶 объявлен внутри <#VIEW> компонента Child и не виден проецируемому содержимому. Вместо этого проецируемое содержимое видит 🐳 кита.

Возможно, вы задаётесь вопросом, почему проецируемый <app-inspector> всё ещё видит 🐳 из App's viewProviders. Причина в том, что Angular DI отслеживает место объявления компонента, а не место его отображения. <app-inspector> находится в шаблоне App — внутри <#VIEW> компонента App, — поэтому провайдеры viewProviders компонента App доступны. Проецирование в Child отсекает доступ к viewProviders компонента Child (🐶), но провайдеры 🐳 компонента App по-прежнему доступны при поиске вверх по дереву.

Однако в следующем разделе вывода Inspector является настоящим дочерним компонентом Child. Inspector находится внутри <#VIEW>, поэтому при запросе AnimalService он видит 🐶 собаку.

AnimalService в логическом дереве будет выглядеть так:

<app-root @ApplicationConfig
          @Inject(AnimalService) animal=>"🐳">
  <#VIEW>
  <app-child>
    <#VIEW @Provide(AnimalService="🐶")
    @Inject(AnimalService=>"🐶")>

    <!-- ^^using viewProviders means AnimalService is available in <#VIEW>-->
    <p>Emoji from AnimalService: {{animal.emoji}} (🐶)</p>

    <div class="container">
      <h3>Content projection</h3>
      <app-inspector @Inject(AnimalService) animal=>"🐳">
        <p>Emoji from AnimalService: {{animal.emoji}} (🐳)</p>
      </app-inspector>
    </div>

    <app-inspector>
      <#VIEW @Inject(AnimalService) animal=>"🐶">
      <p>Emoji from AnimalService: {{animal.emoji}} (🐶)</p>
    </
    #VIEW>
    </app-inspector>
  </
  #VIEW>
  </app-child>

</#VIEW>
</app-root>

Проецируемый <app-inspector> получает 🐳, потому что 🐶 относится к представлению Child, а проецируемое содержимое не может получить к нему доступ. 🐳 доступен, поскольку <app-inspector> объявлен в шаблоне App, поэтому он всё ещё может подняться до viewProviders компонента App.

<app-inspector>, находящийся непосредственно в шаблоне Child (не проецируемый), получает 🐶 — он находится внутри <#VIEW>, поэтому ему не нужно пересекать границу.

Видимость предоставленных токенов

Декораторы видимости определяют, где в логическом дереве начинается и заканчивается поиск токена внедрения. Для этого настройку видимости задают в точке внедрения, то есть при вызове inject(), а не в месте объявления.

Чтобы изменить место, с которого инжектор начинает поиск FlowerService, добавьте skipSelf к вызову inject() <app-child>, куда внедряется FlowerService. Этот вызов является инициализатором свойства в <app-child>, как показано в child.ts:

flower = inject(FlowerService, {skipSelf: true});

С skipSelf инжектор <app-child> не ищет FlowerService в самом себе. Вместо этого он начинает поиск FlowerService в ElementInjector компонента <app-root>, где ничего не находит. Затем он возвращается к ModuleInjector <app-child> и находит значение 🌺 красного гибискуса, доступное благодаря тому, что <app-child> и <app-root> используют один и тот же ModuleInjector. Интерфейс отображает следующее:

Emoji from FlowerService: 🌺

В логическом дереве эта же идея может выглядеть так:

<app-root @ApplicationConfig
          @Inject(FlowerService) flower=>"🌺">
  <#VIEW>
  <app-child @Provide(FlowerService="🌻" )>
    <#VIEW @Inject(FlowerService, SkipSelf)=>"🌺">

    <!-- With SkipSelf, the injector looks to the next injector up the tree (app-root) -->

  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

Хотя <app-child> предоставляет 🌻 подсолнуха, приложение отображает 🌺 красного гибискуса, потому что skipSelf заставляет текущий инжектор (app-child) пропустить себя и обратиться к родительскому.

Если теперь добавить host (в дополнение к skipSelf), результатом будет null. Это происходит потому, что host ограничивает верхнюю границу поиска элементом <#VIEW> app-child. Вот как это выглядит в логическом дереве:

<app-root @ApplicationConfig
          @Inject(FlowerService) flower=>"🌺">
  <#VIEW> <!-- end search here with null-->
  <app-child @Provide(FlowerService="🌻" )> <!-- start search here -->
    <#VIEW inject(FlowerService, {skipSelf: true, host: true, optional:true})=>null>
  </
  #VIEW>
  </app-parent>
</#VIEW>
</app-root>

Здесь сервисы и их значения те же, но host не позволяет инжектору искать FlowerService дальше <#VIEW>, поэтому он не находит его и возвращает null.

skipSelf и viewProviders

Помните, что <app-child> предоставляет AnimalService в массиве viewProviders со значением dog 🐶. Поскольку инжектору нужно проверять только <app-child> компонента ElementInjector на наличие AnimalService, он никогда не увидит 🐳 кита.

Как и в примере с FlowerService, если добавить skipSelf в inject() компонента AnimalService, инжектор не будет искать AnimalService в ElementInjector текущего компонента <app-child>. Вместо этого инжектор начнёт поиск в ElementInjector <app-root>.

@Component({
  selector: 'app-child',
  …
  viewProviders: [
    { provide: AnimalService, useValue: { emoji: '🐶' } },
  ],
})

Логическое дерево с skipSelf в <app-child> выглядит так:

<app-root @ApplicationConfig
          @Inject(AnimalService=>"🐳")>
  <#VIEW><!-- search begins here -->
  <app-child>
    <#VIEW @Provide(AnimalService="🐶")
    @Inject(AnimalService, SkipSelf=>"🐳")>

    <!--Add skipSelf -->

  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

С skipSelf в <app-child> инжектор начинает поиск AnimalService в ElementInjector <app-root> и находит whale 🐳.

host и viewProviders

Если для внедрения AnimalService использовать только host, результатом будет dog 🐶, потому что инжектор находит AnimalService в самом <#VIEW> <app-child>. Конфигурация Child задаёт для viewProviders значение в виде эмодзи собаки AnimalService. Также можно увидеть host inject():

@Component({
  selector: 'app-child',
  …
  viewProviders: [
    { provide: AnimalService, useValue: { emoji: '🐶' } },
  ]
})
export class Child {
  animal = inject(AnimalService, { host: true })
}

host: true заставляет инжектор искать до тех пор, пока он не достигнет границы <#VIEW>.

<app-root @ApplicationConfig
          @Inject(AnimalService=>"🐳")>
  <#VIEW>
  <app-child>
    <#VIEW @Provide(AnimalService="🐶")
    inject(AnimalService, {host: true}=>"🐶")> <!-- host stops search here -->
  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

Добавьте массив viewProviders с третьим животным — ёжиком 🦔 — в метаданные app.ts @Component():

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrls: [ './app.css' ],
  viewProviders: [
    { provide: AnimalService, useValue: { emoji: '🦔' } },
  ],
})

Затем добавьте skipSelf вместе с host в inject() для внедрения AnimalService в child.ts. Вот host и skipSelf при инициализации свойства animal:

export class Child {
  animal = inject(AnimalService, {host: true, skipSelf: true});
}

Когда host и skipSelf применялись к FlowerService, находящемуся в массиве providers, результатом было null, потому что skipSelf начинает поиск в инжекторе <app-child>, но host останавливает поиск на уровне <#VIEW>, где нет FlowerService. В логическом дереве видно, что FlowerService виден в <app-child>, а не в его <#VIEW>.

Однако AnimalService, предоставляемый в массиве viewProviders App, виден.

Представление логического дерева объясняет почему:

<app-root @ApplicationConfig
          @Inject(AnimalService=>"🐳")>
  <#VIEW @Provide(AnimalService="🦔")
  @Inject(AnimalService, @Optional)=>"🦔">

  <!-- ^^skipSelf starts here,  host stops here^^ -->
  <app-child>
    <#VIEW @Provide(AnimalService="🐶")
    inject(AnimalService, {skipSelf:true, host: true, optional: true})=>"🦔">
    <!-- Add skipSelf ^^-->
  </
  #VIEW>
  </app-child>
</#VIEW>
</app-root>

skipSelf заставляет инжектор начинать поиск AnimalService с <app-root>, а не с <app-child>, откуда поступает запрос, а host останавливает поиск на уровне <#VIEW> <app-root>. Поскольку AnimalService предоставляется через массив viewProviders, инжектор находит ёжика 🦔 в <#VIEW>.

Пример: варианты использования ElementInjector

Возможность настраивать один или несколько провайдеров на разных уровнях открывает полезные возможности.

Сценарий: изоляция сервиса

Архитектурные причины могут побудить вас ограничить доступ к сервису областью приложения, к которой он относится. Например, предположим, что мы создаём VillainsList для отображения списка злодеев. Данные о злодеях он получает из VillainsService.

Если предоставить VillainsService в корневом AppModule, VillainsService станет доступен во всём приложении. Если позднее изменить VillainsService, можно нарушить работу других компонентов, которые случайно начали зависеть от этого сервиса.

Вместо этого следует предоставить VillainsService в метаданных providers компонента VillainsList, как показано ниже:

@Component({
  selector: 'app-villains-list',
  templateUrl: './villains-list.html',
  providers: [VillainsService],
})
export class VillainsList {}

Если предоставить VillainsService в метаданных VillainsList и больше нигде, сервис будет доступен только в VillainsList и его дереве подкомпонентов.

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

Сценарий: несколько сеансов редактирования

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

Чтобы продемонстрировать этот сценарий, представьте HeroList, отображающий список супергероев.

Чтобы открыть налоговую декларацию героя, специалист нажимает на его имя, после чего открывается компонент для редактирования декларации. Для каждой выбранной декларации героя открывается отдельный компонент, и одновременно можно открыть несколько деклараций.

Каждый компонент налоговой декларации обладает следующими характеристиками:

  • Является отдельным сеансом редактирования налоговой декларации
  • Позволяет изменить налоговую декларацию, не затрагивая декларацию в другом компоненте
  • Позволяет сохранить изменения в налоговой декларации или отменить их

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

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

import {inject, Service} from '@angular/core';
import {HeroTaxReturn} from './hero';
import {HeroesService} from './heroes.service';

@Service({autoProvided: false})
export class HeroTaxReturnService {
  private currentTaxReturn!: HeroTaxReturn;
  private originalTaxReturn!: HeroTaxReturn;

  private heroService = inject(HeroesService);

  set taxReturn(htr: HeroTaxReturn) {
    this.originalTaxReturn = htr;
    this.currentTaxReturn = htr.clone();
  }

  get taxReturn(): HeroTaxReturn {
    return this.currentTaxReturn;
  }

  restoreTaxReturn() {
    this.taxReturn = this.originalTaxReturn;
  }

  saveTaxReturn() {
    this.taxReturn = this.currentTaxReturn;
    this.heroService.saveTaxReturn(this.currentTaxReturn).subscribe();
  }
}

Вот HeroTaxReturn, использующий HeroTaxReturnService.

import {Component, input, output} from '@angular/core';
import {HeroTaxReturn} from './hero';
import {HeroTaxReturnService} from './hero-tax-return.service';

@Component({
  selector: 'app-hero-tax-return',
  templateUrl: './hero-tax-return.html',
  styleUrls: ['./hero-tax-return.css'],
  providers: [HeroTaxReturnService],
})
export class HeroTaxReturn {
  message = '';

  close = output<void>();

  get taxReturn(): HeroTaxReturn {
    return this.heroTaxReturnService.taxReturn;
  }

  taxReturn = input.required<HeroTaxReturn>();

  constructor() {
    effect(() => {
      this.heroTaxReturnService.taxReturn = this.taxReturn();
    });
  }

  private heroTaxReturnService = inject(HeroTaxReturnService);

  onCanceled() {
    this.flashMessage('Canceled');
    this.heroTaxReturnService.restoreTaxReturn();
  }

  onClose() {
    this.close.emit();
  }

  onSaved() {
    this.flashMessage('Saved');
    this.heroTaxReturnService.saveTaxReturn();
  }

  flashMessage(msg: string) {
    this.message = msg;
    setTimeout(() => (this.message = ''), 500);
  }
}

Налоговая декларация для редактирования передаётся через свойство input, реализованное с помощью геттеров и сеттеров. Сеттер инициализирует собственный экземпляр HeroTaxReturnService компонента входящей декларацией. Геттер всегда возвращает текущее состояние героя, сообщаемое этим сервисом. Компонент также просит сервис сохранить и восстановить эту налоговую декларацию.

Это не будет работать, если сервис является синглтоном всего приложения. Все компоненты будут использовать один экземпляр сервиса, и каждый компонент будет перезаписывать налоговую декларацию, принадлежащую другому герою.

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

providers: [HeroTaxReturnService];

У HeroTaxReturn есть собственный провайдер HeroTaxReturnService. Напомним, что у каждого экземпляра компонента есть собственный инжектор. Предоставление сервиса на уровне компонента гарантирует, что каждый экземпляр компонента получит собственный закрытый экземпляр сервиса. Это предотвращает перезапись налоговых деклараций.

ПОЛЕЗНО: В остальном коде сценария используются другие возможности и методы Angular, о которых можно узнать в других разделах документации.

Сценарий: специализированные провайдеры

Ещё одна причина повторно предоставить сервис на другом уровне — заменить его более специализированной реализацией глубже в дереве компонентов.

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

Корневой инжектор, обозначенный как (A), использует универсальные провайдеры для получения сведений о CarService и EngineService.

  1. Компонент Car (A). Компонент (A) отображает данные об обслуживании шин автомобиля и указывает универсальные сервисы для предоставления дополнительной информации об автомобиле.

  2. Дочерний компонент (B). Компонент (B) определяет собственные специализированные провайдеры для CarService и EngineService, обладающие специальными возможностями, подходящими для задач компонента (B).

  3. Дочерний компонент (C), являющийся дочерним для компонента (B). Компонент (C) определяет собственный, ещё более специализированный провайдер для CarService.

Компонент A

Компонент B

Компонент C

За кулисами каждый компонент настраивает собственный инжектор с нулём, одним или несколькими провайдерами, определёнными для самого компонента.

При разрешении экземпляра Car в самом глубоком компоненте (C) его инжектор создаёт:

  • Экземпляр Car, разрешённый инжектором (C)
  • Экземпляр Engine, разрешённый инжектором (B)
  • Его Tires, разрешённый корневым инжектором (A).

(A) RootInjector

CarService, EngineService, TiresService

(B) ParentInjector

CarService2, EngineService2

(C) ChildInjector

CarService3

(C) Car

(B) Engine

(A) Tires

Подробнее о внедрении зависимостей

Провайдеры DI

Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/di/hierarchical-dependency-injection

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API