Иерархические инжекторы¶
30.09.2026
В этом руководстве подробно разобрана иерархическая система инъекции зависимостей Angular: правила разрешения, модификаторы и продвинутые схемы.
Базовые понятия иерархии инжекторов и области видимости провайдеров — в руководстве по определению провайдеров зависимостей.
Виды иерархий инжекторов¶
В Angular две иерархии инжекторов:
| Иерархии инжекторов | Подробности |
|---|---|
Иерархия EnvironmentInjector | EnvironmentInjector в этой иерархии настраивают через @Service() или массив providers в ApplicationConfig. |
Иерархия ElementInjector | Создаётся неявно у каждого элемента DOM. По умолчанию ElementInjector пуст, пока его не настроят в свойстве providers у @Directive() или @Component(). |
Приложения на NgModule
В приложениях на NgModule зависимости предоставляют через иерархию ModuleInjector с помощью аннотаций @NgModule() или @Injectable().
EnvironmentInjector¶
EnvironmentInjector настраивают одним из двух способов:
- декоратором
@Service() - массивом
providersвApplicationConfig
Tree-shaking и @Service()
Декоратор @Service() предпочтительнее массива providers в ApplicationConfig. С @Service инструменты оптимизации делают tree-shaking и удаляют сервисы, которыми приложение не пользуется. Сборка получается меньше.
Tree-shaking особенно полезен для библиотеки: приложению, которое её подключает, этот сервис может быть не нужен.
EnvironmentInjector настраивается через ApplicationConfig.providers.
Сервис предоставляют через @Service() так:
1 2 3 4 5 6 | |
Декораторы @Service() и @Injectable() помечают класс сервиса.
ModuleInjector¶
В приложениях на NgModule инжектор ModuleInjector настраивают одним из двух способов:
- декоратором
@Service() - свойством
providedInу@Injectable(), со значениемrootилиplatform - массивом
providersу@NgModule()
ModuleInjector настраивается свойствами @NgModule.providers и NgModule.imports. ModuleInjector — это плоский список всех массивов провайдеров, до которых можно дойти, рекурсивно следуя по NgModule.imports.
Дочерние иерархии ModuleInjector появляются при ленивой загрузке других @NgModule.
Платформенный инжектор¶
Выше root есть ещё два инжектора: дополнительный EnvironmentInjector и NullInjector().
Посмотрите, как Angular запускает приложение в main.ts:
1 | |
Метод bootstrapApplication() создаёт дочерний инжектор платформенного инжектора. Его настраивает экземпляр ApplicationConfig. Это корневой EnvironmentInjector.
Метод platformBrowserDynamic() создаёт инжектор, настроенный модулем PlatformModule. В нём зависимости, специфичные для платформы. Так несколько приложений делят одну платформенную конфигурацию. Например, в браузере одна адресная строка, сколько бы приложений ни было запущено. Дополнительные платформенные провайдеры задают на уровне платформы: в функцию platformBrowser() передают extraProviders.
Следующий родитель в иерархии — NullInjector(), вершина дерева. Если поиск дошёл до сервиса в NullInjector(), будет ошибка, если не указан @Optional(). Дерево заканчивается на NullInjector(): без @Optional() он возвращает ошибку, с @Optional() — null. Подробнее об @Optional() — в разделе @Optional() этого руководства.
Схема ниже показывает связь корневого ModuleInjector с родительскими инжекторами, как описано выше.
1 2 3 4 5 6 7 8 | |
Имя root — особый псевдоним. У остальных иерархий EnvironmentInjector псевдонимов нет. Иерархию EnvironmentInjector можно создать в момент появления динамически загружаемого компонента. Так делает маршрутизатор: он создаёт дочерние иерархии EnvironmentInjector.
Все запросы поднимаются к корневому инжектору. Неважно, настроен ли он экземпляром ApplicationConfig, переданным в bootstrapApplication(), или все провайдеры зарегистрированы с root в собственных сервисах.
@Injectable() и ApplicationConfig
Если провайдер на всё приложение задан в ApplicationConfig у bootstrapApplication, он перекрывает провайдер, настроенный для root в метаданных @Injectable(). Так задают нестандартный провайдер сервиса, общего для нескольких приложений.
Пример: конфигурация маршрутизатора компонентов подключает нестандартную стратегию расположения. Её провайдер указан в списке providers у ApplicationConfig.
1 | |
В приложениях на NgModule провайдеры на всё приложение задают в providers модуля AppModule.
ElementInjector¶
Angular неявно создаёт иерархии ElementInjector для каждого элемента DOM.
Сервис в декораторе @Component(), в свойстве providers или viewProviders, настраивает ElementInjector. Например, TestComponent настраивает ElementInjector так:
1 2 3 4 5 | |
Связь дерева EnvironmentInjector, ModuleInjector и дерева ElementInjector разобрана в разделе правил разрешения.
Сервис, предоставленный в компоненте, доступен через ElementInjector этого экземпляра компонента. По правилам видимости из раздела правил разрешения он может быть виден и дочерним компонентам и директивам.
Когда экземпляр компонента уничтожается, уничтожается и экземпляр этого сервиса.
@Directive() и @Component()¶
Компонент — частный случай директивы. У @Directive() есть свойство providers, и у @Component() оно тоже есть. И директивы, и компоненты настраивают провайдеры через providers. Провайдер, заданный в providers компонента или директивы, принадлежит ElementInjector этого компонента или директивы. Компоненты и директивы на одном элементе делят один инжектор.
Правила разрешения¶
Когда токен разрешается для компонента или директивы, Angular делает это в два этапа:
- По родителям в иерархии
ElementInjector. - По родителям в иерархии
EnvironmentInjector.
Когда компонент объявляет зависимость, Angular сначала пытается удовлетворить её собственным ElementInjector компонента. Если у инжектора компонента нет провайдера, запрос уходит к ElementInjector родительского компонента.
Запросы поднимаются, пока Angular не найдёт инжектор, который может обработать запрос, или пока не кончатся предки в иерархиях ElementInjector.
Если провайдера нет ни в одной иерархии ElementInjector, Angular возвращается к элементу, откуда пришёл запрос, и ищет в иерархии EnvironmentInjector. Если провайдера нет и там, выбрасывается ошибка.
Если провайдер одного и того же DI-токена зарегистрирован на разных уровнях, Angular берёт первый, который встретит. Например, если провайдер зарегистрирован локально в компоненте, которому нужен сервис, Angular не ищет другой провайдер того же сервиса.
В приложениях на NgModule Angular ищет в иерархии ModuleInjector, если провайдер не найден в иерархиях ElementInjector.
Модификаторы разрешения¶
Поведение разрешения меняют флаги optional, self, skipSelf и host. Каждый импортируют из @angular/core и передают в конфигурацию inject в момент инъекции сервиса.
Виды модификаторов¶
Модификаторы разрешения делятся на три группы:
- Что делать, если Angular не нашёл нужное:
optional - Откуда начинать поиск:
skipSelf - Где поиск останавливать:
hostиself
По умолчанию Angular всегда начинает с текущего Injector и идёт вверх до конца. Модификаторы меняют начальную точку, то есть self, и конечную.
Модификаторы можно сочетать все, кроме таких пар:
hostиselfskipSelfиself
optional¶
optional помечает инжектируемый сервис как необязательный. Если во время выполнения его нельзя разрешить, Angular подставляет null, а не выбрасывает ошибку. В примере ниже сервис OptionalService не предоставлен ни в сервисе, ни в ApplicationConfig, ни в @NgModule(), ни в классе компонента, поэтому в приложении его нет нигде.
src/app/optional/optional.ts
1 2 3 | |
self¶
self заставляет Angular смотреть только в ElementInjector текущего компонента или директивы.
Хороший случай для self — внедрить сервис, только если он есть на текущем элементе-хосте. Чтобы в этой ситуации не было ошибки, self сочетают с optional.
В SelfNoData ниже обратите внимание на внедрённый LeafService как на свойство.
1 2 3 4 5 6 7 8 | |
В этом примере родительский провайдер есть, и обычная инъекция сервиса вернёт значение. Инъекция с self и optional вернёт null: self говорит инжектору остановить поиск на текущем элементе-хосте.
Другой пример — класс компонента с провайдером FlowerService. Инжектор не идёт дальше текущего ElementInjector: он находит FlowerService и возвращает тюльпан 🌷.
src/app/self/self.ts
1 2 3 4 5 6 7 8 9 | |
skipSelf¶
skipSelf — противоположность self. С skipSelf Angular начинает поиск сервиса в родительском ElementInjector, а не в текущем. Если родительский ElementInjector использует для emoji значение папоротника 🌿, а в массиве providers компонента лежит кленовый лист 🍁, Angular проигнорирует кленовый лист 🍁 и возьмёт папоротник 🌿.
В коде это выглядит так. Пусть родительский компонент использует такое значение emoji из сервиса:
1 2 3 | |
В дочернем компоненте другое значение, кленовый лист 🍁, но нужно значение родителя. Здесь и нужен skipSelf.
skipself.ts
1 2 3 4 5 6 7 8 9 10 11 | |
В этом случае значение emoji будет папоротником 🌿, а не кленовым листом 🍁.
Параметр skipSelf вместе с optional¶
skipSelf вместе с optional не даёт ошибке возникнуть, если значение равно null.
В примере ниже сервис Person внедряется при инициализации свойства. skipSelf говорит Angular пропустить текущий инжектор, а optional не даст ошибке возникнуть, если сервиса Person нет и значение равно null.
1 2 3 | |
host¶
host назначает компонент последней остановкой в дереве инжекторов при поиске провайдеров.
Даже если экземпляр сервиса есть выше по дереву, Angular дальше не пойдёт. host используют так:
host.ts
1 2 3 4 5 6 7 8 9 10 11 | |
У Host указан параметр host, поэтому какое бы значение flower.emoji ни было у родителя Host, сам Host возьмёт тюльпан 🌷.
Модификаторы при инъекции через конструктор¶
Так же, как выше, поведение инъекции через конструктор меняют декораторы @Optional(), @Self(), @SkipSelf() и @Host().
Каждый импортируют из @angular/core и ставят в конструкторе класса компонента в момент инъекции сервиса.
self-no-data.ts
1 2 3 | |
Логическая структура шаблона¶
Сервисы, предоставленные в классе компонента, видны в дереве ElementInjector относительно того, где и как они предоставлены.
Логическая структура шаблона Angular — основа для настройки сервисов и управления их видимостью.
Компоненты используют в шаблонах, как в примере:
1 | |
Обычно компоненты и их шаблоны объявляют в разных файлах. Чтобы понять, как работает инъекция, полезно смотреть на них как на одно логическое дерево. Слово логическое отделяет его от дерева отрисовки — DOM-дерева приложения. Места, где лежат шаблоны компонентов, в этом руководстве помечены псевдоэлементом <#VIEW>. В дереве отрисовки его нет, он нужен только как мысленная модель.
Пример того, как деревья представлений <app-root> и <app-child> складываются в одно логическое дерево:
1 2 3 4 5 6 7 8 9 | |
Граница <#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 — красный гибискус 🌺.
flower.service.ts
1 2 3 4 | |
Возьмём приложение, где есть только App и Child. Самый простой отрисованный вид — вложенные HTML-элементы:
1 2 3 4 | |
За кадром, когда разрешаются запросы инъекции, Angular использует логическое представление:
1 2 3 4 5 6 7 8 | |
<#VIEW> здесь — экземпляр шаблона. У каждого компонента свой <#VIEW>.
Эта структура подсказывает, как предоставлять и внедрять сервисы, и даёт полный контроль над их видимостью.
Пусть <app-root> внедряет FlowerService:
1 2 3 | |
В шаблон <app-root> добавляют привязку, чтобы увидеть результат:
1 | |
В представлении будет:
1 | |
В логическом дереве это выглядит так:
1 2 3 4 5 6 7 8 9 10 | |
Когда <app-root> запрашивает FlowerService, инжектор должен разрешить токен FlowerService. Разрешение идёт в два этапа:
-
Инжектор определяет в логическом дереве, откуда начинать поиск и где его заканчивать. Поиск идёт от начальной точки и проверяет токен на каждом уровне представления логического дерева. Если токен найден, он возвращается.
-
Если токен не найден, инжектор ищет ближайший родительский
EnvironmentInjectorи передаёт запрос ему.
В этом примере ограничения такие:
-
Начать с
<#VIEW>, который принадлежит<app-root>, и закончить на<app-root>.- Обычно поиск начинается в точке инъекции. Здесь
<app-root>— компонент. У@Componentесть ещё и собственныеviewProviders, поэтому поиск начинается с<#VIEW>, который принадлежит<app-root>. Для директивы на том же месте это было бы не так. - Конечная точка совпадает с самим компонентом: это самый верхний компонент приложения.
- Обычно поиск начинается в точке инъекции. Здесь
-
EnvironmentInjectorизApplicationConfigслужит запасным инжектором, если токен инъекции не найден в иерархияхElementInjector.
Массив providers¶
В классе Child добавляют провайдер FlowerService, чтобы в следующих разделах показать более сложные правила разрешения:
1 2 3 4 5 6 7 8 9 10 11 | |
Теперь FlowerService предоставлен в декораторе @Component(). Когда <app-child> запрашивает сервис, инжектору достаточно дойти до ElementInjector в <app-child>. Дальше по дереву инжекторов искать не нужно.
Следующий шаг — привязка в шаблоне Child.
1 | |
Чтобы отрисовать новые значения, <app-child> добавляют в конец шаблона App. Тогда в представлении виден и подсолнух:
1 2 | |
В логическом дереве это выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Когда <app-child> запрашивает FlowerService, инжектор начинает поиск с <#VIEW>, который принадлежит <app-child> (<#VIEW> входит в поиск, потому что инъекция идёт из @Component()), и заканчивает на <app-child>. Здесь FlowerService разрешается в массиве providers компонента <app-child> значением подсолнуха 🌻. Дальше по дереву инжекторов смотреть не нужно. Инжектор останавливается, как только находит FlowerService, и красный гибискус 🌺 не видит.
Массив viewProviders¶
Массив viewProviders — ещё один способ предоставить сервисы в декораторе @Component(). С viewProviders сервисы видны в <#VIEW>.
Шаги те же, что для массива providers, только вместо него берут массив viewProviders.
Пошаговое продолжение — в этом разделе. Если пример уже собран самостоятельно, переходите к разделу Изменение доступности сервиса.
Для демонстрации viewProviders собирают AnimalService. Сначала создают AnimalService со свойством emoji — кит 🐳:
1 2 3 4 5 6 | |
По той же схеме, что и FlowerService, AnimalService внедряют в класс App:
1 2 3 4 | |
Код, связанный с FlowerService, можно оставить: по нему удобно сравнивать с AnimalService.
В класс <app-child> тоже добавляют массив viewProviders и внедряют AnimalService, но emoji задают другое значение. Здесь это собака 🐶.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Привязки добавляют в шаблоны Child и App. В шаблон Child:
1 | |
То же самое — в шаблон App:
1 | |
В браузере видны оба значения:
1 2 3 4 5 | |
Логическое дерево для этого примера с viewProviders:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Как и в примере с FlowerService, AnimalService предоставлен в декораторе @Component() компонента <app-child>. Инжектор сначала смотрит в ElementInjector компонента и находит значение AnimalService — собаку 🐶. Продолжать поиск по дереву ElementInjector не нужно, и в ModuleInjector тоже.
providers и viewProviders¶
Поле viewProviders по смыслу близко к providers, но есть важное отличие. Провайдеры из viewProviders видны только внутри собственного представления компонента. Содержимое, спроецированное в компонент через <ng-content>, их не видит.
Чтобы увидеть разницу между providers и viewProviders, в пример добавляют ещё один компонент и называют его Inspector. Inspector будет потомком Child. В inspector.ts при инициализации свойств внедряют FlowerService и AnimalService:
1 2 3 4 | |
Массивы providers и viewProviders здесь не нужны. Дальше в inspector.html добавляют ту же разметку, что у предыдущих компонентов:
1 2 | |
Inspector нужно добавить в массив imports компонента Child.
1 2 3 4 | |
Дальше в child.html добавляют следующее:
1 2 3 4 5 6 7 8 9 | |
<ng-content> проецирует содержимое, а <app-inspector> внутри шаблона Child делает Inspector дочерним компонентом Child.
Чтобы воспользоваться проекцией содержимого, в app.html добавляют следующее:
1 2 3 | |
Браузер отрисовывает следующее. Предыдущие примеры для краткости опущены:
1 2 3 4 5 6 7 8 | |
Эти четыре привязки показывают разницу между providers и viewProviders. Собака 🐶 объявлена внутри <#VIEW> компонента Child и не видна спроецированному содержимому. Спроецированное содержимое видит кита 🐳.
Спроецированный <app-inspector> всё ещё видит 🐳 из viewProviders компонента App. Для DI в Angular важно место объявления компонента. <app-inspector> живёт в шаблоне App — внутри <#VIEW> компонента App, — поэтому viewProviders компонента App ему доступны. Проекция в Child отрезает доступ к viewProviders компонента Child (🐶), но провайдеры App (🐳) по дереву по-прежнему достижимы.
В следующем фрагменте вывода Inspector — настоящий дочерний компонент Child. Inspector находится внутри <#VIEW>, поэтому при запросе AnimalService он видит собаку 🐶.
AnimalService в логическом дереве выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
Спроецированный <app-inspector> получает 🐳, потому что 🐶 принадлежит представлению Child, и спроецированное содержимое до него не дотягивается. 🐳 доступен, потому что <app-inspector> объявлен в шаблоне App и всё ещё может подняться к viewProviders компонента App.
<app-inspector>, который лежит прямо в шаблоне Child (не спроецирован), получает 🐶: он внутри <#VIEW>, границы пересекать не нужно.
Как дать спроецированному содержимому доступ к инжектору представления¶
Содержимое, спроецированное через <ng-content>, не видит viewProviders компонента: Angular разрешает инъекцию по инжектору того места, где содержимое объявлено, а не где оно отрисовано.
Если спроецированное содержимое должно достучаться до сервиса уровня представления — или до самого экземпляра проецирующего компонента — содержимое принимают как шаблон, а не через <ng-content>, и отрисовывают с явным инжектором.
Child обновляют: AnimalService предоставляют в viewProviders, запрашивают спроецированный шаблон и отрисовывают его через NgTemplateOutlet, а в ngTemplateOutletInjector передают инжектор, который этот сервис видит.
1 2 3 4 5 6 7 8 9 10 11 12 | |
Потребитель оборачивает проецируемую разметку в <ng-template>:
1 2 3 4 5 | |
Теперь Child создаёт <app-inspector> через NgTemplateOutlet своим инжектором, поэтому AnimalService разрешается в собаку 🐶, хотя разметка написана в шаблоне App. За это на обеих сторонах чуть больше разметки, чем у <ng-content>.
Видимость предоставленных токенов¶
Декораторы видимости задают, где в логическом дереве начинается и заканчивается поиск токена инъекции. Настройку видимости ставят в точке инъекции, то есть при вызове inject(), а не в точке объявления.
Чтобы сдвинуть место, откуда инжектор ищет FlowerService, в вызов inject() у <app-child>, где внедряется FlowerService, добавляют skipSelf. Этот вызов — инициализатор свойства в <app-child>, как в child.ts:
1 | |
С skipSelf инжектор <app-child> не ищет FlowerService у себя. Поиск начинается в ElementInjector компонента <app-root>, где ничего нет. Затем инжектор возвращается к ModuleInjector компонента <app-child> и находит красный гибискус 🌺. Это значение доступно, потому что <app-child> и <app-root> делят один ModuleInjector. Интерфейс отрисовывает следующее:
1 | |
В логическом дереве та же идея выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Хотя <app-child> предоставляет подсолнух 🌻, приложение отрисовывает красный гибискус 🌺: skipSelf заставляет текущий инжектор (app-child) пропустить себя и смотреть в родителя.
Если теперь добавить host (вместе со skipSelf), результатом будет null. host ограничивает верхнюю границу поиска <#VIEW> компонента app-child. В логическом дереве это выглядит так:
1 2 3 4 5 6 7 8 9 10 | |
Сервисы и их значения те же, но host не даёт инжектору искать FlowerService дальше <#VIEW>. Токен не находится, и возвращается null.
skipSelf и viewProviders¶
Напомним: <app-child> предоставляет AnimalService в массиве viewProviders со значением собаки 🐶. Инжектору достаточно посмотреть в ElementInjector компонента <app-child>, и кита 🐳 он не видит.
Как в примере с FlowerService, если добавить skipSelf к inject() для AnimalService, инжектор не будет искать AnimalService в ElementInjector текущего <app-child>. Поиск начнётся в ElementInjector компонента <app-root>.
1 2 3 4 5 6 7 | |
Логическое дерево со skipSelf в <app-child>:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Со skipSelf в <app-child> инжектор начинает поиск AnimalService в ElementInjector компонента <app-root> и находит кита 🐳.
host и viewProviders¶
Если для инъекции AnimalService указать только host, результатом будет собака 🐶: инжектор находит AnimalService в самом <#VIEW> компонента <app-child>. Child настраивает viewProviders так, что значением AnimalService служит эмодзи собаки. host виден и в inject():
1 2 3 4 5 6 7 8 9 10 | |
host: true заставляет инжектор искать, пока он не дойдёт до края <#VIEW>.
1 2 3 4 5 6 7 8 9 10 11 | |
В метаданные @Component() файла app.ts добавляют массив viewProviders с третьим животным — ежом 🦔:
1 2 3 4 5 6 7 8 | |
Дальше к inject() для AnimalService в child.ts добавляют skipSelf вместе с host. host и skipSelf в инициализации свойства animal:
1 2 3 | |
Когда host и skipSelf применили к FlowerService из массива providers, результатом был null: skipSelf начинает поиск в инжекторе <app-child>, а host останавливает его на <#VIEW>, где FlowerService нет. В логическом дереве FlowerService виден в <app-child>, а не в его <#VIEW>.
AnimalService, предоставленный в массиве viewProviders компонента App, при этом виден.
Почему так — видно по логическому дереву:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
skipSelf заставляет инжектор начать поиск AnimalService с <app-root>, а не с <app-child>, откуда пришёл запрос. host останавливает поиск на <#VIEW> компонента <app-root>. AnimalService предоставлен через массив viewProviders, поэтому инжектор находит ежа 🦔 в <#VIEW>.
Пример: случаи для ElementInjector¶
Возможность настроить один или несколько провайдеров на разных уровнях открывает полезные схемы.
Сценарий: изоляция сервиса¶
По архитектурным причинам доступ к сервису ограничивают той областью приложения, к которой он относится. Например, VillainsList показывает список злодеев. Злодеев он получает из VillainsService.
Если предоставить VillainsService в корневом AppModule, сервис будет виден во всём приложении. Позже изменение VillainsService может сломать другие компоненты, которые начали от него зависеть случайно.
Вместо этого VillainsService предоставляют в метаданных providers компонента VillainsList:
1 2 3 4 5 6 | |
Если VillainsService предоставлен в метаданных VillainsList и больше нигде, сервис доступен только в VillainsList и его дереве подкомпонентов.
VillainsService — синглтон относительно VillainsList, потому что объявлен там. Пока VillainsList не уничтожен, это один и тот же экземпляр VillainsService. Если экземпляров VillainsList несколько, у каждого свой экземпляр VillainsService.
Сценарий: несколько сеансов редактирования¶
Многие приложения позволяют работать с несколькими открытыми задачами одновременно. Например, в приложении для подготовки налоговых деклараций специалист ведёт несколько деклараций и в течение дня переключается между ними.
Чтобы показать этот сценарий, представьте HeroList со списком супергероев.
Чтобы открыть налоговую декларацию героя, специалист щёлкает по имени. Открывается компонент редактирования этой декларации. Каждая выбранная декларация открывается в своём компоненте, и несколько деклараций могут быть открыты сразу.
У каждого компонента декларации такие свойства:
- Это отдельный сеанс редактирования налоговой декларации
- Декларацию можно менять, не затрагивая декларацию в другом компоненте
- Изменения своей декларации можно сохранить или отменить
Допустим, у HeroTaxReturn есть логика, которая ведёт изменения и откатывает их. Для одной налоговой декларации героя это простая задача. В реальном мире, с богатой моделью данных декларации, учёт изменений сложнее. Эту работу можно отдать вспомогательному сервису, как в примере.
HeroTaxReturnService кэширует одну декларацию HeroTaxReturn, следит за её изменениями и умеет сохранить или восстановить её. Он также делегирует общему для приложения синглтону HeroService, который получает через инъекцию.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
Вот HeroTaxReturn, который пользуется HeroTaxReturnService.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | |
Декларация tax-return-to-edit приходит через свойство input, реализованное геттером и сеттером. Сеттер инициализирует собственный экземпляр HeroTaxReturnService этого компонента входящей декларацией. Геттер всегда возвращает то, что сервис считает текущим состоянием героя. Компонент также просит сервис сохранить и восстановить эту декларацию.
Так не получится, если сервис — синглтон на всё приложение. Все компоненты делили бы один экземпляр сервиса, и каждый затирал бы декларацию другого героя.
Чтобы этого не было, инжектор уровня компонента HeroTaxReturn настраивают так, чтобы он предоставлял сервис. Для этого в метаданных компонента указывают свойство providers.
1 | |
У HeroTaxReturn свой провайдер HeroTaxReturnService. У каждого экземпляра компонента свой инжектор. Сервис на уровне компонента даёт каждому экземпляру компонента собственный экземпляр сервиса. Тогда ни одна декларация не будет затёрта.
Остальной код сценария опирается на другие возможности и приёмы Angular. О них — в других разделах документации.
Сценарий: специализированные провайдеры¶
Ещё одна причина предоставить сервис заново на другом уровне — подставить более специализированную реализацию глубже в дереве компонентов.
Например, компонент Car показывает сведения о шиномонтаже и зависит от других сервисов, которые дают подробности об автомобиле.
Корневой инжектор, помеченный как (A), использует общие провайдеры сведений о CarService и EngineService.
-
Компонент
Car(A). Компонент (A) показывает данные шиномонтажа и задаёт общие сервисы, чтобы дать больше сведений об автомобиле. -
Дочерний компонент (B). Компонент (B) определяет собственные, специализированные провайдеры
CarServiceиEngineServiceс возможностями, которые подходят тому, что происходит в компоненте (B). -
Дочерний компонент (C) как потомок компонента (B). Компонент (C) определяет собственный, ещё более специализированный провайдер
CarService.
1 2 3 4 5 6 7 8 9 10 11 12 | |
За кадром каждый компонент поднимает свой инжектор с нулём, одним или несколькими провайдерами, определёнными для самого компонента.
Когда экземпляр Car разрешают в самом глубоком компоненте (C), его инжектор даёт следующее:
- Экземпляр
Car, разрешённый инжектором (C) Engine, разрешённый инжектором (B)Tires, разрешённые корневым инжектором (A)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
Ещё об инъекции зависимостей¶
Источник: https://angular.dev/guide/di/hierarchical-dependency-injection