angular
March 13

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

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

- Через providers

@Component({ 
  … 
  providers: [ 
    {provide: FlowerService, useValue: {emoji: '🌺'}} 
  ] 
})

- ЧерезviewProviders

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

Чтобы понять, как параметры providers и viewProviders по-разному влияют на видимость сервисов, далее разберем пример, демонстрирующий их использование в логическом дереве. Пример доступен для скачивания по ссылке.

В логическом дереве используются @Provide, @Inject и ApplicationConfig, которые не являются реальными HTML-атрибутами, но демонстрируют происходящие процессы "под капотом".


@Inject(Token)=>Value

Показывает, что если токен инжектируется в данное место логического дерева, его значение будет равно Value.

@Provide(Token=Value)
Показывает объявление провайдера Token с значением Value в данном месте логического дерева.

ApplicationConfig(Token)

Показывает, что в данном месте следует использовать резервный 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>

Emoji from FlowerService: 🌺

В логическом дереве 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. Поиск токена происходит в два этапа:

  1. Инжектор определяет начальное и конечное местоположение поиска в логическом дереве. Инжектор начинает с начального местоположения и ищет токен на каждом уровне логического дерева. Если токен найден, он возвращается.
  2. Если токен не найден, инжектор ищет ближайший родительский 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>

В результате получим такой вывод:

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>

Логическое дерево 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>

в результату чего получим следующий вывод:

AppComponent
Emoji from AnimalService: 🐳

Child Component
Emoji from AnimalService: 🐶

Логическое дерево 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.

Emoji from FlowerService: 🌻
Emoji from AnimalService: 🐳

Emoji from FlowerService: 🌻
Emoji from AnimalService: 🐶

Эти четыре привязки демонстрируют разницу между 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>.