Иерархия инжекторов в Angular. Как устроен шаблон. Часть 2
Продолжим изучение документации с раздела про логическую структуру в шаблонах. Предыдущая часть перевода доступна по ссылке
Логическая структура шаблона
Когда сервис объявляется в массиве providers
класса компонента, он становится доступен в дереве ElementInjector
относительно того, где и как вы предоставляете эти сервисы.
Понимание логической структуры шаблона Angular даст вам основу для настройки сервисов и, в свою очередь, контроля их видимости.
Компоненты используются в ваших шаблонах, как в следующем примере:
<app-root> <app-child></app-child> </app-root>
Обычно компоненты и их шаблоны объявляются в отдельных файлах. Для понимания того, как работает DI, полезно рассматривать их с точки зрения логического дерева. Термин "логическое" отличает его от дерева рендеринга, которое является 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()
.
Класс компонента может регистрировать сервисы двумя способами:
@Component({ … providers: [ {provide: FlowerService, useValue: {emoji: '🌺'}} ] })
@Component({ … viewProviders: [ {provide: AnimalService, useValue: {emoji: '🐶'}} ] })
Чтобы понять, как параметры providers
и viewProviders
по-разному влияют на видимость сервисов, далее разберем пример, демонстрирующий их использование в логическом дереве. Пример доступен для скачивания по ссылке.
В логическом дереве используются @Provide
, @Inject
и ApplicationConfig
, которые не являются реальными HTML-атрибутами, но демонстрируют происходящие процессы "под капотом".
Показывает, что если токен инжектируется в данное место логического дерева, его значение будет равно Value
.
— @Provide(Token=Value)
Показывает объявление провайдера Token
с значением Value
в данном месте логического дерева.
Показывает, что в данном месте следует использовать резервный EnvironmentInjector
.
Пример использования
Пример приложения содержит сервис FlowerService
, предоставленный в корне с значением hibiscus 🌺
.
// src/app/flower.service.ts @Injectable({ providedIn: 'root' }) export class FlowerService { emoji = '🌺'; }
Рассмотрим приложение с компонентами AppComponent
и ChildComponent
. Template будет выглядеть как вложенные HTML-элементы следующего вида:
<app-root> <!-- AppComponent selector --> <app-child> <!-- ChildComponent selector --> </app-child> </app-root>
При поиске DI зависимостей, Angular будет использовать несколько иное представление:
<app-root> <!-- AppComponent selector --> <#VIEW> <app-child> <!-- ChildComponent selector --> <#VIEW> </#VIEW> </app-child> </#VIEW> </app-root>
В данном случае <#VIEW>
представляет собой экземпляр шаблона. Обратите внимание, что у каждого компонента есть свой собственный <#VIEW>
.
Знание этой структуры может помочь определить, как вы предоставляете и внедряете ваши сервисы, и дает вам полный контроль над их видимостью.
Теперь представим, что <app-root>
внедряет сервис FlowerService
:
// src/app/app.component.ts export class AppComponent { constructor(public flower: FlowerService) {} }
Выведем в шаблон значение из сервиса:
<p> Emoji from FlowerService: {{flower.emoji}} </p>
В логическом дереве Angular увидит следующее:
<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
. Поиск токена происходит в два этапа:
- Инжектор определяет начальное и конечное местоположение поиска в логическом дереве. Инжектор начинает с начального местоположения и ищет токен на каждом уровне логического дерева. Если токен найден, он возвращается.
- Если токен не найден, инжектор ищет ближайший родительский
EnvironmentInjector
для делегирования запроса.
В примере выше поиск будет осуществлен в следующем порядке:
- Отправной точкой послужит
<#VIEW>
, принадлежащий<app-root>
, и закончится в самом<app-root>
. В случае<app-root>
существуют собственныеviewProviders
, поэтому поиск начинается с<#VIEW>
, принадлежащего<app-root>
. Это не относится к директиве, сопоставленной на том же уровне. Поиск завершается на самом компоненте, так как он является верхним компонентом в этом приложении. ElementInjector
, предоставленныйApplicationConfig
, действует как резервный инжектор, когда токен инъекции не будет найден в иерархииElementInjector
.
Использование providers
Добавим провайдер FlowerService
в ChildComponent
// src/app/child.component.ts @Component({ standalone: true, selector: 'app-child', templateUrl: './child.component.html', styleUrls: ['./child.component.css'], // use the providers array to provide a service providers: [{ provide: FlowerService, useValue: { emoji: '🌻' } }] }) export class ChildComponent { // inject the service constructor( public flower: FlowerService) { } }
Теперь, когда FlowerService
объявлен в декораторе @Component()
, при запросе сервиса <app-child>
инжектору достаточно будет просмотреть только ElementInjector
в <app-child>
. Ему не придется продолжать поиск дальше по дереву инжекторов.
Следующим шагом добавим вывод в шаблон:
// src/app/child.component.html <p> Emoji from FlowerService: {{flower.emoji}} </p>
В результате получим такой вывод:
<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>
Логическое дерево Angular будет выглядеть следующим образом:
<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>
(включен потому что он внедрен из @Component()
), и заканчивает поиск в <app-child>
. В этом случае FlowerService
возьмет значение из массива providers с sunflower 🌻
. Инжектор не поднимается выше, и не видит hibiscus 🌺
Использование viewProviders
Массив viewProviders
можно использовать как еще один способ объявления сервисов в декораторе @Component()
. Использование viewProviders
делает сервисы видимыми в <#VIEW>
.
Для начала создадим AnimalService
с свойством emoji
, содержащим значок whale🐳
:
// src/app/animal.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class AnimalService { emoji = '🐳'; }
Следуя тому же шаблону, что и с FlowerService
, внедрим AnimalService
в класс AppComponent
:
// src/app/app.component.ts export class AppComponent { constructor(public flower: FlowerService, public animal: AnimalService) {} }
Внедрим AnimalService
в viewProviders
, заменив значение на dog 🐶
// src/app/child.component.ts @Component({ standalone: true, selector: 'app-child', templateUrl: './child.component.html', styleUrls: ['./child.component.css'], // provide services providers: [{ provide: FlowerService, useValue: { emoji: '🌻' } }], viewProviders: [{ provide: AnimalService, useValue: { emoji: '🐶' } }], imports: [InspectorComponent] }) export class ChildComponent { // inject service constructor( public flower: FlowerService, public animal: AnimalService) { } }
и добавим вывод в шаблоны дочернего и родительского компонентов:
// src/app/child.component.html <p>Emoji from AnimalService: {{animal.emoji}}</p> // src/app/app.component.html <p>Emoji from AnimalService: {{animal.emoji}}</p>
в результату чего получим следующий вывод:
Логическое дерево Angular будет выглядеть следующим образом:
<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
объявлен в декораторе @Component()
для <app-child>
. Это означает, что поскольку инжектор сначала ищет в ElementInjector
компонента, он находит значение AnimalService
dog 🐶
. Ему не нужно продолжать поиск в дереве ElementInjector
или ModuleInjector
.
Сравнение providers
и viewProviders
Чтобы увидеть разницу между providers
и viewProviders
, добавим еще один компонент InspectorComponent
. InspectorComponent
будет дочерним компонентом ChildComponent
. В файле inspector.component.ts
будет внедрен FlowerService
и AnimalService
в конструкторе.
// src/app/inspector/inspector.component.ts export class InspectorComponent { constructor(public flower: FlowerService, public animal: AnimalService) { } }
Затем в файле inspector.component.html
добавим ту же разметку из предыдущих компонентов.
<p>Emoji from FlowerService: {{flower.emoji}}</p> <p>Emoji from AnimalService: {{animal.emoji}}</p>
Добавим InspectorComponent
в массив импортов ChildComponent
.
// src/app/child/child.component.ts @Component({ ... imports: [InspectorComponent] })
Далее доработаем child.component.html
:
// src/app/child/child.component.html <p>Emoji from FlowerService: {{flower.emoji}}</p> <p>Emoji from AnimalService: {{animal.emoji}}</p> <div class="container"> <h3>Content projection</h3> <ng-content></ng-content> </div> <h3>Inside the view</h3> <app-inspector></app-inspector>
Первые две строки с привязками остались от предыдущих шагов. Новые части - это <ng-content>
и <app-inspector>
. <ng-content>
позволяет проецировать контент, а <app-inspector>
внутри шаблона ChildComponent
делает InspectorComponent
дочерним компонентом ChildComponent
.
Затем добавим следующее в app.component.html
, чтобы воспользоваться проекцией контента.
// src/app/app.component.html <app-child><app-inspector></app-inspector></app-child>
Теперь результат вывода в браузере будет таким:
//…Omitting previous examples. The following applies to this section.
Content projection: this is coming from content. Doesn't get to see
puppy because the puppy is declared inside the view only.
Эти четыре привязки демонстрируют разницу между providers
и viewProviders
. Поскольку значение dog 🐶
объявлено внутри <#VIEW>, оно не видно в проецируемом контенте. Вместо этого проецируемый контент видит значение whale 🐳
.
Однако в следующем разделе, где InspectorComponent
является дочерним компонентом ChildComponent
, InspectorComponent
находится внутри <#VIEW>
, поэтому при запросе AnimalService
он видит значение dog 🐶
.
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>
видит значение whale 🐳
, а не dog 🐶
, потому что dog 🐶
находится внутри <app-child>
<#VIEW>
. <app-inspector>
может увидеть значение dog 🐶
только если он также находится внутри <#VIEW>
.