Иерархия инжекторов в 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>.