AngularInDepth уходит от Medium. Эта статья, ее обновления и более свежие статьи размещены на новой платформе inDepth.dev

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

Но, может быть, есть еще кое-что, о чем вам следует знать?

Также некоторое время назад в главную ветку была добавлена ​​так называемая функция Tree-Shakeable Tokens. Если вы похожи на меня, вы, вероятно, захотите узнать, что изменилось.

Так что пора все это изучить и, может быть, найти что-то новое ...

Инжекторное дерево

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

Как разработчик я хочу понять, как angular строит дерево инжекторов. Вот как я вижу верхнюю часть дерева инжекторов Angular:

Это не все дерево. На данный момент здесь нет никаких компонентов. Рисовать продолжим позже. Но теперь давайте начнем с AppModule Injector, поскольку это наиболее часто используемая часть angular.

Инжектор модулей корневого приложения

Хорошо известный угловой инжектор корневого приложения представлен на картинке выше как AppModule Injector. Как уже было сказано, этот инжектор собирает всех провайдеров из переходных модулей. Это означает, что:

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

Согласно этому правилу, MyService2 из EagerModule2 будут включены в корневой инжектор.

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

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

Для инициализации инжектора NgModule Angular использует AppModule factory, который находится в так называемом module.ngfactory.js файле.

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

Совет. Если у вас есть приложение angular в режиме разработки и вы хотите видеть всех провайдеров из корневого инжектора AppModule, просто откройте консоль devtools и напишите:

Есть также много хорошо известных фактов, которые я не буду здесь описывать, потому что они хорошо освещены в документации по angular:

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

Как оказалось, корневой инжектор AppModule имеет родительский NgZoneInjector, который является дочерним по отношению к PlatformInjector.

Инжектор платформы обычно включает встроенных поставщиков, но мы можем предоставить свои собственные при создании платформы:

Дополнительные провайдеры, которых мы можем передать платформе, должны быть StaticProviders. Если вы не знакомы с разницей между StaticProvider и Provider, следуйте этому SO-ответу.

Совет. Если у вас есть приложение angular в режиме разработки и вы хотите видеть всех поставщиков из инжектора платформы, просто откройте консоль devtools и напишите:

Несмотря на то, что совершенно ясно, как angular разрешает зависимость от уровня инжектора AppModule и выше, я обнаружил, что это очень запутанная вещь на уровне компонентов. Итак, я начал свое расследование.

EntryComponent и RootData

Когда я говорил о ComponentFactoryResolver, я упомянул entryComponents. Эти типы компонентов обычно передаются либо в bootstrap, либо в entryComponents массиве NgModule. Angular router также создает компонент динамически.

Angular создает хост-фабрики для всех элементов entryComponents, которые являются корневыми представлениями для всех остальных. Это означает, что:

Каждый раз, когда мы создаем динамический компонент, angular создает корневое представление с корневыми данными, которое содержит ссылки на elInjector и ngModule injector.

Теперь предположим, что мы запускаем приложение angular.

Что происходит при выполнении следующего кода?

На самом деле многое происходит в фоновом режиме, но нас интересует та часть, где angular создает компонент входа.

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

Элемент-инжектор против модуля-инжектора

Некоторое время назад, когда начали широко использоваться ленивые загружаемые модули, на github было сообщено об одном странном поведении: система внедрения зависимостей вызвала двойное создание экземпляров ленивых загружаемых модулей. В результате появился новый дизайн. Итак, с этого момента у нас было два параллельных дерева: одно для элементов, а другое для модулей.

Главное правило здесь таково:

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

Обратите внимание: я использую не «инжектор компонентов», а «инжектор элементов».

Что такое инжектор слияния?

Вы когда-нибудь писали такой код?

Итак, инжектор здесь является инжектором слияния (аналогично мы можем ввести инжектор слияния в конструктор компонентов).

Инжектор слияния имеет следующее определение:

Как видно из предыдущего кода, инжектор слияния - это просто комбинация представления и определения элемента. Этот инжектор работает как мост между деревом инжектора элементов и деревом инжектора модуля, когда angular разрешает зависимости.

Инжектор слияния также может разрешать такие встроенные вещи, как ElementRef, ViewContainerRef, TemplateRef, ChangeDetectorRef и т.д. И что более интересно, он может возвращать инжектор слияния.

Практически каждый элемент может иметь инжектор слияния, даже если вы не предоставили для него токен.

Совет: чтобы получить инжектор слияния, просто откройте консоль и напишите:

Но вы можете спросить, что же тогда представляет собой инжектор элемента?

Как мы все знаем, angular анализирует шаблон для создания фабрики с определением представления. Представление - это просто представление шаблона, который содержит различные типы узлов, такие как directive, text, provider, query и т. Д. И среди прочего есть узел элемента. Фактически, инжектор элемента находится на этом узле. Angular хранит всю информацию о поставщиках на узле элемента со следующими свойствами:

Давайте посмотрим, как инжектор элементов разрешает зависимость:

Он просто проверяет allProviders или publicProviders properties в зависимости от конфиденциальности.

Этот инжектор содержит экземпляр компонента / директивы и всех поставщиков, зарегистрированных компонентом или директивами.

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

Например, у Angular есть некоторые ограничения при использовании декоратора Host. Здесь может помочь viewProviders на главном элементе. (См. Также https://medium.com/@a.yurich.zuev/angular-nested-template-driven-form-4a3de2042475).

Другой случай: если у нас есть элемент с примененным к нему компонентом и директивой, и мы предоставляем один и тот же токен для компонента и для директивы, тогда поставщик директивы выигрывает.

Совет: чтобы получить инжектор элементов, просто откройте консоль и напишите:

Алгоритм разрешения

Код, описывающий алгоритм разрешения угловых зависимостей в представлении, можно найти здесь. И это именно то, что инжектор слияния использует в get методе (Services.resolveDep). Чтобы понять, как работает алгоритм разрешения зависимостей, мы должны быть знакомы с концепциями представления и родительского элемента представления.

Если у нас есть корневой компонент приложения с шаблоном <child></child>, то у нас есть три представления:

HostView_AppComponent
    <my-app></my-app>
View_AppComponent
    <child></child>
View_ChildComponent
    some content

Алгоритм разрешения основан на иерархии представлений:

Если мы запросим какой-то токен в дочернем компоненте, он сначала посмотрит на инжектор дочерних элементов, где проверяет elRef.element.allProviders|publicProviders, затем проходит вверх по всем родительским элементам представления (1) , а также проверяет провайдеров в инжектор элемента . Если следующий родительский элемент представления равен нулю (2), то он возвращается к startView (3), проверяет startView.rootData.elnjector (4) и только тогда, если токен не найден, проверяет startView.rootData module.injector ( 5).

То есть Angular ищет родительский элемент определенного представления, а не родительский элемент определенного элемента при прохождении компонентов для разрешения некоторой зависимости. Чтобы получить просмотреть родительский элемент, Angular использует следующую функцию:

Например, представьте себе следующее небольшое угловое приложение:

Предположим, что мы находимся внутри grid-tile компонента и запрашиваем GridListComponent. . Мы сможем успешно получить этот экземпляр компонента. Но как?

Каков родительский элемент представления на данный момент?

Вот шаги, которым я следую, чтобы ответить на этот вопрос:

  1. Найдите начальный элемент. У нашего GridTileComponent есть grid-tile селектор элемента, поэтому нам нужно найти элемент, который соответствует grid-tile селектору. Это grid-tile элемент.
  2. Найдите шаблон, которому принадлежит grid-tile элемент (MyListComponent шаблон).
  3. Определите вид для этого элемента. Если у него нет родительского встроенного представления, то это компонентное представление, в противном случае - встроенное представление. (У нас нет элементов ng-template или *structuralDirective над grid-tile, поэтому в нашем случае это View_MyListComponent).
  4. Найдите просмотреть родительский элемент. То есть родительский элемент для представления, а не для элемента.

Здесь есть два случая:

  • Для встроенного представления это родительский узел, содержащий контейнер представления.

Например, представим, что мы применили структурную директиву к grid-list:

Родительский элемент просмотра для grid-tile в этом случае будет div.container.

  • Для представления компонентов это ведущий элемент

Это то, что у нас есть в нашем первоначальном небольшом приложении. Таким образом, родительский элемент просмотра будет my-list элементом, а не grid-list.

Теперь вы можете задаться вопросом, как angular может разрешить GridListComponent, если он обошел grid-list?

Ключ к пониманию того, как angular собирает провайдеров для элементов: он использует прототипное наследование.

Каждый раз, когда мы предоставляем какой-либо токен для элемента, angular создает новый массив allProviders и publicProviders, унаследованный от родительского узла, в противном случае он просто использует тот же массив с родительским узлом.

Это означает, что grid-tile уже известно обо всех провайдерах, которые были зарегистрированы для всех родительских элементов в текущем представлении.

По сути, Angular собирает провайдеров для элементов в шаблоне следующим образом:

Как мы видим выше, grid-tile может успешно получить GridListComponent от своего инжектора элемента через allProviders, потому что инжектор элемента grid-tile содержит поставщиков из родительского элемента.

Подробнее об этом здесь, в этом ТАК ответе.

Поставщики прототипов наследования для элементов - одна из причин, по которой мы не можем использовать параметр multi для предоставления токенов на нескольких уровнях. Но поскольку внедрение зависимостей - очень гибкая система, есть способ ее обойти. Https://stackoverflow.com/questions/49406615/is-there-a-way-how-to-use-angular-multi-providers-from-all-multiple-levels

Имея все это в виду, пора продолжить рисование нашего дерева инжекторов.

Простое приложение my-app- ›child-› приложение для внуков

Рассмотрим следующее простое приложение:

У нас есть древовидные уровни компонентов, и мы спрашиваем Service в GrandChildComponent.

my-app
   child
      grand-child(ask for Service dependency)

Вот как angular разрешит Service зависимость.

На картинке выше мы начинаем с элемента grand-child, который расположен на View_Child (1). Angular пройдет через все родительские элементы представления. Когда нет родительского элемента представления (в нашем случае my-app не имеет родительских элементов представления), он сначала смотрит на корень elInjector (2):

startView.root.injector в нашем случае - это NullInjector. Поскольку NullInjector не хранит токенов, следующим шагом будет переключение на инжектор модуля (3):

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

AppModule Injector 
        ||
        \/
    ZoneInjector 
        ||
        \/
  Platform Injector 
        ||
        \/
    NullInjector 
        ||
        \/
       Error

Простое маршрутизируемое приложение

Давайте изменим наше приложение и добавим маршрутизатор в ChildComponent.

После этого у нас будет что-то вроде:

my-app
   router-outlet
   child
      grand-child(dynamic creation)

Теперь посмотрим, где роутер создает динамические компоненты:

На этом этапе angular создает новое корневое представление с новым объектом rootData. Мы видим, что angular передает OutletInjector как root elInjector. OutletInjector создается с родительским элементом this.location.injector, который является инжектором для элемента router-outlet.

OutletInjector - это особый вид инжектора, который действует как ссылка между маршрутизируемым компонентом и родительским элементом router-outlet и находится здесь.

Простое приложение с отложенной загрузкой

Наконец, переместим GrandChildComponent в модуль с отложенной загрузкой. Для этого нам нужно добавить router-outlet в представление дочерних компонентов и изменить конфигурацию маршрутизатора, как показано ниже:

my-app
   router-outlet
   child (dynamic creation)
       router-outlet
         +grand-child(lazy loading)

Теперь нарисуем два отдельных дерева для нашего приложения с отложенной загрузкой:

Жетоны встряхивания деревьев уже на горизонте

Angular продолжает работать над уменьшением фреймворка, и начиная с версии 6 он будет поддерживать другой способ регистрации провайдеров.

Инъекционный

Раньше класс с Injectable декоратором не указывал, что он может иметь зависимость, это не было связано с тем, как он будет использоваться в других частях. Итак, если у службы нет зависимости, @Injectable() можно удалить без каких-либо проблем.

Как только API станет стабильным, мы можем настроить Injectable decorator, чтобы сообщать angular, к какому модулю он принадлежит и как его следует создавать:

Вот простой пример того, как мы можем это использовать:

Таким образом, вместо включения всех провайдеров в фабрику ngModule angular хранит информацию о провайдере в метаданных Injectable. Это то, что нам нужно, чтобы наши библиотеки стали меньше. Если мы используем Injectable для регистрации поставщиков, а потребители не импортируют наших поставщиков, он не будет включен в окончательный пакет. Так,

Предпочитайте регистрацию поставщиков в Injectables, а не в NgModule.providers, а не в Component.providers.

Ранее я упоминал токены модулей, которые добавляются в инжектор корневого модуля. Таким образом, angular может различать, какие модули представлены в конкретном модульном инжекторе.

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

InjectionToken

В случае InjectionToken мы также сможем определить, как токен будет создан системой DI и в каких инжекторах он будет доступен.

Итак, его предполагается использовать следующим образом:

Заключение

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