Инъекция зависимостей в действии¶
28.02.2022
В этом руководстве рассматриваются многие возможности инъекции зависимостей (DI) в Angular.
Смотрите живой пример для рабочего примера, содержащего фрагменты кода из этого руководства.
Множественные экземпляры сервисов (песочница)¶
Иногда требуется несколько экземпляров сервиса на одном и том же уровне иерархии компонентов.
Хорошим примером является сервис, хранящий состояние для компонента-компаньона. Для каждого компонента требуется отдельный экземпляр сервиса. Каждый сервис имеет свое собственное рабочее состояние, изолированное от состояния сервиса и состояния другого компонента. Это называется песочницей, поскольку у каждого сервиса и экземпляра компонента есть своя "песочница", в которой можно играть.
В данном примере HeroBiosComponent
представляет три экземпляра HeroBioComponent
.
1 2 3 4 5 6 7 8 |
|
Каждый HeroBioComponent
может редактировать биографию одного героя. HeroBioComponent
полагается на HeroCacheService
для получения, кэширования и выполнения других операций сохранения для данного героя.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Три экземпляра HeroBioComponent
не могут использовать один и тот же экземпляр HeroCacheService
, так как они будут конкурировать друг с другом в определении героя для кэширования.
Вместо этого каждый HeroBioComponent
получает свой собственный экземпляр HeroCacheService
, указывая HeroCacheService
в своем массиве метаданных providers
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Родительский HeroBiosComponent
привязывает значение к heroId
. При ngOnInit
этот идентификатор передается сервису, который извлекает и кэширует героя. Геттер для свойства hero
извлекает кэшированного героя из сервиса. В шаблоне отображается это свойство, связанное с данными.
Найдите этот пример в live code и убедитесь, что три экземпляра HeroBioComponent
имеют свои собственные кэшированные данные о герое.
Квалифицировать поиск зависимостей с помощью декораторов параметров¶
Когда классу требуется зависимость, она добавляется в конструктор в качестве параметра. Когда Angular необходимо инстанцировать класс, он обращается к DI-фреймворку для предоставления зависимости. По умолчанию DI-фреймворк ищет провайдера в иерархии инжекторов, начиная с локального инжектора компонента и при необходимости поднимаясь вверх по дереву инжекторов, пока не достигнет корневого инжектора.
- Первый инжектор, сконфигурированный с провайдером, передает зависимость (экземпляр сервиса или значение) в конструктор.
- Если в корневом инжекторе провайдер не найден, фреймворк DI выдает ошибку.
Существует несколько вариантов модификации стандартного поведения поиска с помощью декораторов параметров на сервисных параметрах конструктора класса.
Сделайте зависимость @Optional
и ограничьте поиск с помощью @Host
.¶
Зависимости могут быть зарегистрированы на любом уровне иерархии компонентов. Когда компонент запрашивает зависимость, Angular начинает с инжектора этого компонента и идет вверх по дереву инжекторов, пока не найдет первого подходящего провайдера. Если во время этого прохода не удается найти зависимость, Angular выдает ошибку.
В некоторых случаях необходимо ограничить поиск или учесть отсутствующую зависимость. Изменить поведение Angular при поиске можно с помощью декораторов @Host
и @Optional
, определяющих значение параметра service в конструкторе компонента.
- Декоратор свойства
@Optional
указывает Angular возвращать null, если он не может найти зависимость -
Декоратор свойства
@Host
останавливает восходящий поиск на хостовом компоненте.Обычно хост-компонент — это компонент, запрашивающий зависимость. Однако если этот компонент проецируется на родительский компонент, то родительский компонент становится хостом. В следующем примере рассматривается второй случай.
Эти декораторы могут использоваться как по отдельности, так и вместе, как показано в примере. Этот HeroBiosAndContactsComponent
является ревизией HeroBiosComponent
, который вы рассматривали выше.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Ориентируйтесь на шаблон:
1 2 3 4 |
|
Теперь между тегами <hero-bio>
появился новый элемент <hero-contact>
. Angular проецирует, или трансклюзирует, соответствующий HeroContactComponent
в представление HeroBioComponent
, помещая его в слот <ng-content>
шаблона HeroBioComponent
.
1 2 3 4 |
|
Результат показан ниже, причем номер телефона героя из HeroContactComponent
проецируется над описанием героя.
Приведем HeroContactComponent
, который демонстрирует квалификационные декораторы.
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 |
|
Ориентируйтесь на параметры конструктора.
1 2 3 4 5 6 |
|
Функция @Host()
, украшающая свойство конструктора heroCache
, обеспечивает получение ссылки на сервис кэширования от родительского компонента HeroBioComponent
. Angular выдает ошибку, если в родительском компоненте отсутствует этот сервис, даже если компонент, расположенный выше в дереве компонентов, содержит его.
Вторая функция @Host()
украшает свойство конструктора loggerService
. Единственный экземпляр LoggerService
в приложении предоставляется на уровне AppComponent
. Хост HeroBioComponent
не имеет собственного провайдера LoggerService
.
Angular выдает ошибку, если вы также не украсили свойство параметром @Optional()
. Если свойство помечено как необязательное, Angular устанавливает значение loggerService
в null, а остальная часть компонента адаптируется.
Вот HeroBiosAndContactsComponent
в действии.
Если закомментировать декоратор @Host()
, то Angular будет подниматься по дереву предков-инжекторов, пока не найдет логгер на уровне AppComponent
. Логика работы логгера срабатывает, и на экране героя появляется маркер "!!!", указывающий на то, что логгер найден.
Если восстановить декоратор @Host()
и закомментировать @Optional
, то приложение будет выбрасывать исключение, когда не сможет найти нужный логгер на уровне хост-компонента.
1 |
|
Предоставление пользовательского провайдера с помощью @Inject
¶
Использование пользовательского провайдера позволяет обеспечить конкретную реализацию неявных зависимостей, таких как встроенные API браузера. В следующем примере используется InjectionToken
для предоставления API браузера localStorage в качестве зависимости в BrowserStorageService
.
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 |
|
Функция factory
возвращает свойство localStorage
, привязанное к объекту окна браузера. Декоратор Inject
— это параметр конструктора, используемый для указания пользовательского провайдера зависимости. Теперь этот пользовательский провайдер может быть переопределен при тестировании с помощью имитации API localStorage
вместо взаимодействия с реальными API браузера.
Модифицируйте поиск провайдера с помощью @Self
и @SkipSelf
.¶
Провайдеры также могут быть скопированы инжектором через декораторы параметров конструктора. Следующий пример переопределяет токен BROWSER_STORAGE
в классе providers
компонента Component
с API браузера sessionStorage
. Один и тот же BrowserStorageService
дважды инжектируется в конструктор, украшенный параметрами @Self
и @SkipSelf
для определения того, какой инжектор обрабатывает зависимость от провайдера.
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 49 50 51 52 53 54 55 56 57 |
|
При использовании декоратора @Self
инжектор обращается к инжектору компонента только для поиска его провайдеров. Декоратор @SkipSelf
позволяет пропустить локальный инжектор и поискать в иерархии провайдер, удовлетворяющий данной зависимости. Экземпляр sessionStorageService
взаимодействует с BrowserStorageService
, используя API браузера sessionStorage
, а localStorageService
пропускает локальный инжектор и использует корневой BrowserStorageService
, который использует API браузера localStorage
.
Инжектировать DOM-элемент компонента¶
Хотя разработчики стараются этого избегать, многие визуальные эффекты и сторонние инструменты, такие как jQuery, требуют доступа к DOM. В результате может потребоваться доступ к DOM-элементу компонента.
В качестве иллюстрации приведем минимальную версию HighlightDirective
со страницы Attribute Directives.
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 |
|
Директива устанавливает фон на цвет выделения, когда пользователь наводит курсор мыши на элемент DOM, к которому применена директива.
При этом Angular устанавливает параметр el
конструктора в инжектированный ElementRef
. ("ElementRef" — это обертка вокруг элемента DOM, свойство nativeElement
которой раскрывает элемент DOM для манипулирования директивой).
В примере к двум тегам <div>
применяется атрибут директивы appHighlight
, сначала без значения (цвет по умолчанию), а затем с заданным значением цвета.
1 2 3 4 5 6 |
|
На следующем рисунке показан эффект от наведения курсора мыши на тег <hero-bios-and-contacts>
.
Определение поставщиков¶
Зависимость не всегда может быть создана стандартным методом инстанцирования класса. О некоторых других методах вы узнали в разделе Dependency Providers. Следующий пример HeroOfTheMonthComponent
демонстрирует многие из альтернатив и то, зачем они нужны. Визуально он прост: несколько свойств и журналы, создаваемые логгером.
Код, стоящий за ним, настраивает, как и где фреймворк DI предоставляет зависимости. Примеры использования иллюстрируют различные способы применения объектного литерала provide для связывания объекта определения с маркером DI.
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 |
|
Массив providers
показывает, как можно использовать различные ключи определения провайдера: useValue
, useClass
, useExisting
или useFactory
.
Провайдеры значений: useValue
.¶
Ключ useValue
позволяет связать фиксированное значение с маркером DI. Эта техника используется для предоставления констант конфигурации времени выполнения, таких как базовые адреса сайтов и флаги возможностей. Также можно использовать провайдер значений в модульном тесте для предоставления имитационных данных вместо производственного сервиса данных.
В примере HeroOfTheMonthComponent
имеется два провайдера значений.
1 2 |
|
-
Первый предоставляет существующий экземпляр класса
Hero
для использования в качестве маркераHero
, а не требует от инжектора создавать новый экземпляр с помощьюnew
или использовать свой собственный кэшированный экземпляр.Здесь маркером является сам класс.
-
Во втором случае для маркера
TITLE
используется литеральный строковый ресурс.Токен провайдера
TITLE
не является классом, а представляет собой специальный вид ключа поиска провайдера, называемый injection token, представленный экземпляромInjectionToken
.
Токен инъекции можно использовать для любого типа провайдера, но он особенно полезен, когда зависимость представляет собой простое значение, например строку, число или функцию.
Значение провайдера значений должно быть определено до того, как вы укажете его здесь. Строковый литерал title
доступен сразу. Переменная someHero
в этом примере была задана ранее в файле, как показано ниже. Нельзя использовать переменную, значение которой будет определено позже.
1 2 3 4 5 6 |
|
Другие типы провайдеров могут создавать свои значения одномоментно, то есть тогда, когда они нужны для инъекции.
Провайдеры классов: useClass
.¶
Ключ провайдера useClass
позволяет создавать и возвращать новый экземпляр указанного класса.
Этот тип провайдера можно использовать для подстановки альтернативной реализации общего класса или класса по умолчанию. Альтернативная реализация может, например, реализовать другую стратегию, расширить класс по умолчанию или эмулировать поведение реального класса в тестовом случае.
В следующем коде показаны два примера в HeroOfTheMonthComponent
.
1 2 |
|
Первый провайдер представляет собой упрощенную, расширенную форму наиболее типичного случая, когда создаваемый класс (HeroService
) является также маркером инъекции зависимостей провайдера. Как правило, предпочтительнее использовать короткую форму; в этой длинной форме все детали выражены явно.
Второй провайдер заменяет DateLoggerService
на LoggerService
. LoggerService
уже зарегистрирован на уровне AppComponent
. Когда этот дочерний компонент запрашивает LoggerService
, он получает вместо него экземпляр DateLoggerService
.
Этот компонент и дерево его дочерних компонентов получают экземпляр DateLoggerService
. Компоненты вне дерева продолжают получать оригинальный экземпляр LoggerService
.
DateLoggerService
наследует от LoggerService
; он добавляет текущую дату/время к каждому сообщению:
1 2 3 4 5 6 7 8 9 10 11 |
|
Псевдонимы провайдеров: useExisting
.¶
Ключ провайдера useExisting
позволяет сопоставить один токен с другим. По сути, первый маркер является алиасом для сервиса, связанного со вторым маркером, создавая два способа доступа к одному и тому же объекту сервиса.
1 |
|
Этот прием можно использовать для сужения API через интерфейс псевдонимов. В следующем примере показан псевдоним, введенный для этой цели.
Представьте, что LoggerService
имеет большой API, гораздо больший, чем реальные три метода и свойство. Возможно, вы захотите сократить эту поверхность API до тех членов, которые вам действительно нужны. В этом примере MinimalLogger
class-interface сокращает API до двух членов:
1 2 3 4 5 6 |
|
В следующем примере MinimalLogger
используется в упрощенной версии HeroOfTheMonthComponent
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Параметр logger
конструктора HeroOfTheMonthComponent
типизирован как MinimalLogger
, поэтому в редакторе, поддерживающем TypeScript, видны только члены logs
и logInfo
.
За кулисами Angular устанавливает параметр logger
в полный сервис, зарегистрированный под токеном LoggingService
, которым оказался экземпляр DateLoggerService
, который был предоставлен выше.
Провайдеры фабрики: useFactory
¶
Ключ провайдера useFactory
позволяет создать объект зависимости путем вызова функции-фабрики, как показано в следующем примере.
1 |
|
Инжектор предоставляет значение зависимости, вызывая фабричную функцию, которую вы указываете в качестве значения ключа useFactory
. Обратите внимание, что у этой формы провайдера есть третий ключ, deps
, который задает зависимости для функции useFactory
.
С помощью этой техники можно создать объект зависимости с фабричной функцией, входные данные которой представляют собой комбинацию инжектируемых сервисов и локальных состояний.
Объект зависимости (возвращаемый фабричной функцией) обычно представляет собой экземпляр класса, но может быть и другим. В данном примере объект зависимости — это строка с именами победителей конкурса "Герой месяца".
В примере локальное состояние — это число 2
, количество участников конкурса, которое должен показать компонент. Значение состояния передается в качестве аргумента в runnersUpFactory()
. Функция runnersUpFactory()
возвращает фабричную функцию провайдера, которая может использовать как переданное значение состояния, так и инжектированные сервисы Hero
и HeroService
.
1 2 3 4 |
|
Функция фабрики провайдеров (возвращаемая функцией runnersUpFactory()
) возвращает фактический объект зависимости — строку имен.
-
В качестве аргументов функция принимает победителя
Hero
иHeroService
.Angular предоставляет эти аргументы из инжектируемых значений, идентифицируемых двумя токенами в массиве
deps
. -
Функция возвращает строку имен, которую Angular затем инжектирует в параметр
runnersUp
компонентаHeroOfTheMonthComponent
.
Функция получает из HeroService
героев-кандидатов, принимает 2
из них за победителей и возвращает их скомбинированные имена. Полный исходный текст смотрите в живом примере.
Альтернативы маркера поставщика: интерфейс класса и 'InjectionToken'¶
Инъекция зависимостей в Angular наиболее проста, когда маркер провайдера представляет собой класс, который также является типом возвращаемого объекта зависимости или сервиса.
Однако токен не обязательно должен быть классом, и даже если он является классом, он не обязательно должен быть того же типа, что и возвращаемый объект. Этому посвящен следующий раздел.
Интерфейс класса¶
В предыдущем примере Hero of the Month в качестве маркера для провайдера LoggerService
использовался класс MinimalLogger
.
1 |
|
MinimalLogger
— это абстрактный класс.
1 2 3 4 5 6 |
|
Абстрактный класс обычно является базовым классом, который можно расширять. В данном приложении, однако, нет класса, который бы наследовался от MinimalLogger
. Сервисы LoggerService
и DateLoggerService
могли бы наследоваться от MinimalLogger
или реализовать его в виде интерфейса. Но они не сделали ни того, ни другого. MinimalLogger
используется только в качестве маркера для инъекции зависимостей.
Когда вы используете класс таким образом, это называется интерфейсом класса.
Как уже упоминалось в Configuring dependency providers, интерфейс не является корректным маркером DI, поскольку это артефакт TypeScript, не существующий во время выполнения. Используйте этот абстрактный интерфейс класса, чтобы получить сильную типизацию интерфейса, а также использовать его в качестве маркера провайдера так же, как и обычный класс.
Интерфейс класса должен определять только те члены, которые разрешено вызывать его потребителям. Такой сужающийся интерфейс помогает отделить конкретный класс от его потребителей.
Использование класса в качестве интерфейса позволяет получить характеристики интерфейса в реальном объекте JavaScript. Однако для минимизации затрат памяти класс не должен иметь реализации. Для конструктора MinimalLogger
транспилируется в этот неоптимизированный, предварительно минимизированный JavaScript.
1 2 3 4 5 |
|
У него нет членов. Он никогда не вырастет, сколько бы членов вы ни добавили в класс, если эти члены типизированы, но не реализованы.
Посмотрите еще раз на класс TypeScript MinimalLogger
, чтобы убедиться, что у него нет реализации.
Объекты 'InjectionToken'¶
Объекты зависимостей могут представлять собой простые значения, такие как даты, числа и строки, или бесформенные объекты, такие как массивы и функции.
Такие объекты не имеют прикладных интерфейсов и поэтому не могут быть представлены классом. Их лучше представлять с помощью токена, который одновременно уникален и символичен — объекта JavaScript, имеющего дружественное имя, но не конфликтующего с другим токеном, который случайно имеет такое же имя.
Такими характеристиками обладает InjectionToken
. Вы дважды встречали их в примере Герой месяца, в поставщике значения title и в поставщике фабрики runnersUp.
1 2 |
|
Вы создали токен TITLE
следующим образом:
1 2 3 |
|
Параметр type
, хотя и является необязательным, передает тип зависимости разработчикам и инструментальным средствам. Описание маркера является еще одним вспомогательным параметром для разработчиков.
Инжектирование в производный класс¶
Будьте внимательны при написании компонента, наследующего от другого компонента. Если базовый компонент имеет инжектированные зависимости, то их необходимо заново предоставить и инжектировать в производном классе, а затем передать в базовый класс через конструктор.
В данном примере SortedHeroesComponent
наследуется от HeroesBaseComponent
для отображения отсортированного списка героев.
Компонент HeroesBaseComponent
может быть самостоятельным. Он требует свой экземпляр HeroService
для получения героев и отображает их в порядке поступления из базы данных.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Сохраняйте конструкторы простыми¶
Конструкторы должны делать не более чем инициализацию переменных. Это правило позволяет безопасно конструировать компонент при тестировании, не опасаясь, что он сделает что-то драматическое, например, обратится к серверу. Именно поэтому вы вызываете HeroService
из ngOnInit
, а не из конструктора.
Пользователи хотят видеть героев в алфавитном порядке. Вместо того чтобы модифицировать исходный компонент, создайте его подкласс и SortedHeroesComponent
, который сортирует героев перед их отображением. Компонент SortedHeroesComponent
позволяет базовому классу получать героев.
К сожалению, Angular не может инжектировать HeroService
непосредственно в базовый класс. Вы должны предоставить HeroService
еще раз для этого компонента, а затем передать его базовому классу внутри конструктора.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Теперь обратите внимание на метод afterGetHeroes()
. Первый инстинкт — создать метод ngOnInit
в SortedHeroesComponent
и выполнить сортировку там. Но Angular вызывает ngOnInit
производного класса перед вызовом ngOnInit
базового класса, поэтому сортировка массива героев будет происходить до их появления. Это приводит к неприятной ошибке.
Переопределение метода afterGetHeroes()
базового класса решает эту проблему.
Эти сложности говорят в пользу отказа от наследования компонентов.
Разрешение круговых зависимостей с помощью прямой ссылки на класс (forwardRef)¶
Порядок объявления классов имеет значение в TypeScript. Вы не можете напрямую ссылаться на класс, пока он не определен.
Обычно это не является проблемой, особенно если вы придерживаетесь рекомендуемого правила один класс на файл. Но иногда круговые ссылки неизбежны. Например, когда класс 'A' ссылается на класс 'B', а 'B' ссылается на 'A'. Один из них должен быть определен первым.
Функция Angular forwardRef()
создает непрямую ссылку, которую Angular может разрешить позже.
Пример Parent Finder полон круговых ссылок на классы, которые невозможно разорвать.
Вы сталкиваетесь с этой дилеммой, когда класс делает ссылку на самого себя, как это делает AlexComponent
в своем массиве providers
. Массив providers
является свойством функции-декоратора @Component()
, которая должна появиться выше определения класса.
Разорвите замкнутый круг с помощью forwardRef
.
1 |
|