Dependency injection¶
Все сервисы регистрируются Injector-ом, который является частью механизма DI в Angular. Причем в приложении может быть несколько injector-ов одновременно.
Когда компоненту требуется сервис, то его поиск начинается с самого нижнего injector-а и далее вверх по иерархии, то есть сначала проверяется уровень самого компонента.
Если сервис был найден на одном из нижних уровней, то дальнейший поиск не осуществляется. Если же поиск вообще не даст никаких результатов, то будет сгенерирована ошибка.
Декораторы @SkipSelf() и @Optional()¶
Немного выше уже упоминались два нюанса, связанных с работой Angular Dependency Injection. Первая из них это прекращение поиска, если запрошенный сервис определен на уровне компонента. Но что если вам понадобится, например, обратиться из этого компонента к двум сервисам одновременно: локальному и глобальному?
Angular Providers¶
Самый простой способ зарегистрировать сервис в приложении.
Также в качестве элемента массива свойства Angular providers можно передать объект конфигурации сервиса.
Последние два примера полностью идентичны друг другу.
Конфигурационный объект позволяет переопределить значение для конкретного injection token.
Возможные свойства объекта конфигурации:
В deps перечисляются все зависимости, необходимые для создания factory provider.
Все значения, указываемые в свойстве provide должны существовать в приложении и быть классами, иначе будет неизвестно, что принимать за injection token.
Для того чтобы сделать возможным внедрение в компонент сущность, не являющуюся сервисом Angular, нужно определить для нее вручную injection token.
Возможности Angular DI, о которых почти ничего не сказано в документации
Angular — это достаточно большой фреймворк. Задокументировать и написать примеры использования для каждого кейса просто невозможно. И механизм внедрения зависимостей не исключение. В этой статье я расскажу о возможностях Angular DI, о которых вы почти ничего не найдете в официальной документации.
Что вы знаете о функции inject?
Документация говорит нам следующее:
Injects a token from the currently active injector. Must be used in the context of a factory function such as one defined for an InjectionToken. Throws an error if not called from such a context.
И дальше мы видим использование функции inject в примере с tree shakable токеном:
Это все, что говорит нам документация.
В фабриках tree shakable провайдеров.
В фабриках провайдеров.
В конструкторах сервисов.
В конструкторах модулей.
Так как фабричная функция InjectionToken не может иметь аргументы, inject — единственный способ получить данные из инжектора. Но зачем нам эта функция в сервисах, если можно просто указать необходимые зависимости прямо в параметрах конструктора?
Рассмотрим небольшой пример.
Ручная установка контекста для функции inject
Если внимательно посмотреть, то работа функции inject становится совершенно очевидной:
вызов функции setCurrentInjector присваивает в приватную переменную _currentInjector переданный инжектор, возвращая предыдущий;
функция inject достает из _currentInjector значение по переданному токену.
Это настолько просто, что мы совершенно спокойно можем заставить работать функцию inject даже в компонентах и директивах:
Правда, выглядит это совершенно неюзабельно. Да и использование приватных функций Angular тоже не входит в лучшие практики. Поэтому не советую заниматься подобными вещами.
Injection flags
InjectFlags — это аналог модификаторов Optional, Self, SkipSelf и Host. Используются в функциях inject и Injector.get. Документация и здесь не подвела — ее почти нет:
Человек знающий сразу увидит здесь битовые маски. Этот же enum можно представить немного в другом виде:
Использование одного флага
Вот так можно получить поток событий роутера, не беспокоясь о том, что модуль роутинга не подключен:
Выглядит просто. А на деле — еще и безопасно, без неожиданных падений и лишних событий.
Комбинация флагов
Комбинацию флагов можно использовать при проверке, что модуль импортировался один раз. А комбинируются они при помощи побитового ИЛИ:
Значение нужного бита получается с помощью побитового И:
Tree shakable сервисы и *SansProviders
Tree shakable сервисы — это специальный способ сказать компилятору, что сервис не нужно включать в сборку, если он нигде не используется. При этом сервис не указывается в модуле, скорее наоборот — модуль указывается в сервисе:
Как мы видим из примера, в проперти providedIn указан модуль. Это работает точно так же, как и следующий пример:
Самый полезный, по моему мнению, способ использования фабрики в tree shakable сервисах выглядит так:
Плюсы такого определения сервиса:
Исключено случайное использование сервиса. Для работы с ним необходимо импортировать модуль сервиса.
Гарантировано создание одного экземпляра сервиса.
С остальными способами определения не все так очевидно. Я могу предположить, что это поможет при использовании внешних библиотек, в которых еще и отсутствует типизация.
Для остальных типов провайдеров я предлагаю придумать use-кейсы вам самим. Буду рад, если вы поделитесь ими в комментариях.
Взаимодействие компонент
В документации перечислены все существующие способы взаимодействия компонент и директив между собой:
Input binding with a setter, ngOnChanges.
Child events (outputs).
Но ни слова не сказано о том, что дочерняя директива/компонент совершенно спокойно может получить инстанс родителя через DI.
UPD: нашлась все таки ссылочка на документацию, где описывается этот способ.
На примере видно, что дочерняя директива получает инстанс компонента родителя, упрощая их взаимодействие. Обрабатывая клик по хостовому элементу, директива может напрямую поменять значение родительского компонента. Использование любого другого способа было бы как минимум более многословным.
Но почему это возможно? Ответ может быть очень большим, и он явно выходит за рамки этой статьи. Когда-нибудь я напишу об этом более подробно. А пока предлагаю вам прочитать об иерархии инжекторов и ElementInjector (именно в таком порядке).
Вместо вывода
Как я уже говорил, Angular — очень большой фреймворк. И я уверен, что в его исходниках можно найти намного больше интересных возможностей.
Своими находками я всегда делюсь в своем Твиттере. Например, советы по Angular вы можете найти по хэштегу #AngularTip. А перевод самых интересных твитов — по хэштегу #AngularTipRus! Буду рад, если вы поделитесь своими наблюдениями и советами со мной и сообществом. Спасибо за внимание!
Список хороших статей об Angular DI
Я вспомнил еще 2 статьи об Angular DI от @MarsiBarsi на русском языке. Пишите где угодно, предлагайте и давайте его пополнять!
Angular 2 и внедрение зависимостей
Вторая версия ангуляра всё ближе к релизу и всё больше людей начинают интересоваться им. Пока что информации по фреймворку не так много, особенно на русском языке, и всё чаще начинают возникать вопросы по некоторым темам.
Одна из тем, вызывающая много вопросов — внедрение зависимостей. Некоторые люди не сталкивались с подобной технологией. Другие не до конца понимают, как она работает в рамках Angular 2, так как привыкли к другим реализациям, которые бывают в других фреймворках.
А разгадка кроется в том, что DI во втором ангуляре действительно несколько отличается от других, и связано это в первую очередь с общим подходом и философией 2-й версии. Заключается она в том, что сущностями из которых строится всё приложение, являются компоненты. Сервисный слой, роутер, система внедрения зависимостей — вторичны и они имеют смысл только в рамках компонента. Это очень важный момент, который лежит в основе понимания архитектуры нового фреймворка.
Введение
Это пересказ 2-х страниц из оф. документации, касательно внедрению зависимостей в Angular 2: этой и этой.
В статье я буду использовать Typescript. Почему?
Сам фреймворк написан на Typescript, и информации по связке Angular2 + Typescript больше всего.
Код на Typescript с точки зрения синтаксиса — это свежая реализация стандарта ES, дополнительная типизация, и немного вспомогательных фишек. Тем не менее, приложения можно писать и на Javascript, и на Dart. В JS-версии можно не использовать ES6+ синтаксис, однако теряется лаконичность и ясность кода. А если настроить Babel на поддержку свежих фич, то синтаксически всё будет очень похоже на TS-код: классы, аннотации/декораторы, и т.д. Ну только без типов, так что внедрение зависимостей будет выглядеть немного по-другому.
Проблема зависимостей
Представьте, что мы пишем некое абстрактное приложение, разделяя код на небольшие логические кусочки (чтобы не возникло путаницы с терминологией ангуляра, я не буду их называть «компонентами», пускай это будут просто классы-сервисы, в которых содержится бизнес-логика).
Конечно, логики тут нет совсем, но для иллюстрации вполне подойдёт.
Итак, в чём тут проблема? На данный момент Car жёстко зависит от 2-х сервисов, которые вручную создаются в его конструкторе. С точки зрения потребителя сервиса Car это хорошо, ведь зависимость Car сама позаботилась о своих зависимостях. Но, если мы, например, захотим сделать, чтобы в конструктор Engine передавался обязательный параметр, то придётся менять и код самого Car :
Стало быть, создавать экземпляры зависимостей в потребителе не так уж хорошо.
Перепишем код так, чтобы экземпляры зависимостей Car передавались извне:
Уже получше. Код самого сервиса сократился, а сам сервис стал более гибким. Его легче тестировать и конфигурировать:
И с каждым новым компонентом и каждой новой зависимостью всё труднее создавать экземпляры сервисов. Можно конечно сделать фабрику, в которую вынести всю логику по созданию сервиса Car :
На пути к внедрению
Как можно улучшить код? Каждый потребитель знает о том, какие сервисы-зависимости ему нужны. Но чтобы уменьшить связность системы, потребитель не должен создавать их сам. Можно создать класс-синглтон, в котором бы создавались и хранились инстансы всех наших сервисов. В таком классе мы определяем, как нужно создавать необходимые сервисы, а получать их можно, например, по некому ключу. Тогда в сервисах достаточно будет только как-то получить экземпляр такого синглтона, а из него уже получать готовые инстансы зависимостей. Такой паттерн называется ServiceLocator. Это одна из разновидности инверсии контроля. Тогда код выглядел бы примерно так:
С одной стороны, мы избавились от жёсткой связи потребителя и его зависимостей: все сервисы создаются вне потребителей. Но с другой, теперь потребители жёстко связаны с сервис-локатором: каждый потребитель должен знать, где находится инстанс сервис-локатора. Ну и создание сервисов происходит по-прежнему вручную.
Хотелось бы, чтобы можно было просто каким-то образом указать в потребителе его зависимости и переменную, в которую будет помещён экземпляр зависимости, а создание и внедрение самих сервисов сделать автоматическим.
Этим и занимаются DI-фреймворки. Они управляют жизненным циклом внедряемых зависимостей, отслеживают места, где эти зависимости требуются и внедряют их, т.е. передают в потребителя созданный инстанс зависимости, которую потребитель запросил. Из потребителей исчезает жёсткая зависимость от сервис-локатора: теперь с локатором работает DI-фреймворк.
Суть работы примерно такая:
И в зависимости от DI-фреймворка, эти пункты будут выглядеть в коде по-разному.
Ангуляр №1
Для более лучшего понимания устройства второй версии этого фреймворка, в частности, DI, я хотел бы немного описать, как устроена первая его часть.
Жизненный цикл приложения состоит из нескольких этапов. Я хотел бы выделить 2 этапа:
На верхнем уровне находятся модули. Модуль, по-сути, — просто объект, в котором могут регистрироваться и храниться различные части приложения: сервисы, контроллеры, директивы, фильтры. Так же, у модуля могут быть config- и run-колбеки, которые запустятся на соответствующих этапах приложения.
Итак, как же выглядит внедрение зависимостей в первой версии:
Да, тут есть куча всяких нюансов. Например, в первом ангуляре есть аж 5 разных видов сервисов, так что зарегистрировать сервис можно разными способами. А при минификации кода аргументы функции могут измениться, так что лучше использовать другой синтаксис для объявления зависимостей.
Но я не хочу углубляться в дебри первого ангуляра, напишу лишь основные моменты:
Angular 2: новый путь
Вторая версия ангуляра была заявлена как новый фреймворк, написанный с нуля, в котором учли все ошибки первой части. По мере использования 2-й версии у меня сложилось именно такое впечатление. Лишние сущности и концепции исчезли. То, что осталось, стало только лучше и удобнее, а нововведения хорошо вписываются и выглядят логичными.
Первый ангуляр был, по-сути, просто набором полезных приёмов, техник и паттернов, склееных с помощью DI вместе. Но отдельные его части были как-то сами по себе, были слегка разрозненны. Не было единой концепции.
В итоге, структура приложения могла быть совершенно разной. Но вместо свободы обычно это означало смешивание концепций.
Компонентный подход
Поле selector содержит строку, которая будет использоваться в качестве css-селектора для поиска компонента в DOM. Можно передать любой валидный селектор, но чаще всего используют селектор-тэг, не входящий в стандартный набор HTML-тэгов. Таким образом, создаются кастомные тэги.
Поле template содержит строку-шаблон, которым заменится содержимое DOM-элемента, найденного по селектору. Вместо строки с шаблоном можно передать строку с путём до файла-шаблона (только поле будет называться templateUrl ). Подробнее про синтаксис шаблонов можно почитать страницу доков или её русский перевод.
Иерархия компонентов
Что было плохого в первом ангуляре? Там была иерархия скоупов, но сервисный слой был общий для всех. Сервисы настраивались раз и навсегда до запуска приложения, да ещё и были синглтонами.
Ещё были проблемы с роутерами. Оригинальный был довольно скуден, не позволял создавать нормальной иерархии. UI-router был более богат на фичи, позволял использовать несколько view, умел строить иерархию состояний.
Но основная проблема обоих роутеров заключалась в том, что вся эта иерархия путей была абсолютно никак не связана с иерархией скоупов и была крайне не гибкой.
Как же поступили во второй версии? В основе второго ангуляра, как я уже говорил, лежат компоненты. Всё приложение состоит только из компонентов, которые образуют древовидную иерархическую структуру. Корневой компонент загружается с помощью функции bootstrap на HTML-страницу (если используется браузер как целевая платформа). Все остальные компоненты помещаются внутрь корневого и образуют дерево компонентов.
Как же сделать так, чтобы каждый компонент мог бы быть максимально независимым, переиспользуемым и самодостаточным, при этом, избежать дублирования кода?
Чтобы обеспечить компоненту независимость, у него есть метаданные, позволяющие полностью описать всё, что нужно для работы этому компоненту: настройку роутинга, список используемых директив, пайпов и сервисов. Чтобы не быть связанным через сервисный слой, каждый компонент теперь имеет свой роутер и свой инжектор. И они, в свою очередь, так же образуют иерархию, которая всегда связана с иерархией компонентов.
Это и отличает DI в Angular2 от других DI-фреймворков: в ангуляре у приложения нет одного инжектора, у каждого компонента может быть свой инжектор
Внедрение зависимостей в Angular2
Как же выглядит внедрение зависимостей во втором ангуляре? Сервисы теперь внедряются по типу. Внедрение обычно происходит в конструктор потребителя.
Сервисы
Сервис в Angular 2 — это простой класс.
Регистрация сервисов
Чтобы сервис можно было внедрить, сперва нужно зарегистрировать его. Нам не нужно вручную создавать инжектор, ангуляр сам создаёт глобальный инжектор, когда вызывается функция bootstrap :
Вторым аргументом можно передать массив, содержащий провайдеры. Так что один из способов сделать сервис доступным — добавить его класс в список:
Этот код сделает наш сервис доступным для всего приложения. Однако так делать не всегда хорошо. Разработчики фреймворка советуют регистрировать в этом месте только системные провайдеры, и только если они нужны во всей системе. Например, провайдеры роутера, форм и Http-сервисов.
Второй способ зарегистрировать сервис — добавить его в метаданные компонента в поле providers:
Внедрение сервисов в компонент
Самый простой способ внедрить сервис — через конструктор. Так как TypeScript поддерживает типы, то достаточно написать так:
И всё! Если UserService был зарегистрирован, то ангуляр внедрит нужный инстанс в аргумент конструктора.
Внедрение сервисов в сервисы
Так что добавляем Logger в список провайдеров компонента:
Опциональные зависимости
Если внедряемый сервис не обязателен, то нужно добавить аннотацию @Optional :
Провайдеры
Когда мы добавляем класс сервиса в список провайдеров (компонента или в функцию bootstrap), на деле это означает следующее:
На самом деле, когда я говорил, что внедрение происходит по типу, я сказал не всю правду. Токеном, по которому может происходить внедрение может быть не только класс, но об этом немного позже.
Альтернативные провайдеры сервисов
Даже если альтернативный класс имеет какую-то зависимость, которой нет у оригинального сервиса:
Мы всё равно сможем так же просто использовать его, нужно лишь зарегистрировать нужные зависимости:
Алиасы провайдеров
То получится не то, что мы хотели: создадутся 2 экземпляра нового логгера. Один будет использоваться там, где внедряется старый, другой — где внедряется новый логгер. Чтобы создался только 1 инстанс нового логгера, который бы использовался везде, регистрируем провайдер с опцией useExisting :
Провайдеры значений
Иногда проще не создавать отдельный класс, чтобы заменить им провайдер сервиса, а просто использовать готовое значение. Например:
Чтобы использовать уже готовый объект, регистрируем провайдер с опцией useValue :
Провайдер-фабрика / фабричный провайдер
Иногда нужно зарегистрировать провайдер динамически, используя информацию, недоступную с самого начала. Например, эта информация может быть получена из сессии и быть разной от раза к разу. Также предположим, что внедряемый сервис не имеет независимого доступа к этой информации.
В таких случаях используют провайдер-фабрику / фабричный провайдер.
Чтобы использовать фабрику, регистрируем провайдер, передав в поле useFactory наше фабрику, а в поле deps — зависимости этой фабрики:
Токены внедрения зависимостей
Когда мы регистрируем какой-то провайдер в инжекторе, мы ассоциируем этот провайдер с неким токеном, по которому потом получаем зависимость. Обычно, в роли токена выступает класс. Мы бы могли вручную получать инстанс зависимости, используя инжектор напрямую:
Это происходит автоматически, когда мы пишем в конструкторе что-то такое:
Всё потому что ангуляр сам может достать тип аргумента из конструктора и получить по нему зависимость у инжектора.
Неклассовые зависимости
Но что если наша зависимость не является классом? Например, мы хотим внедрить строку, функцию, объект и т.д.
Например, часто надо внедрять объект-конфиг, который будут использовать другие сервисы. Но мы хотим, чтобы этот объект реализовывал определённый интерфейс, чтобы было меньше проблем из-за несоответствия типов:
Мы уже конфигурировали провайдер так, чтобы он возвращал уже созданный объект. Попробуем сделать так же:
Но так сделать не выйдет: интерфейсы не могут быть токенами для инжектора.
Это выглядит странным, ведь в Java или C# мы чаще всего внедряем именно интерфейс (а DI-фреймворк находит нужную его реализацию), а не класс. Но тут такой штуки не выйдет. И это вина не ангуляра, а самого JavaScript. Дело в том, что interface — это фича TypeScript, и существует он только на этапе компиляции. В рантайме нет никаких интерфейсов, так что внедрить интерфейс тайпскрипта мы не сможем.
Решение проблемы
Чтобы внедрить такую зависимость, используем аннотацию @Inject :
В итоге, мы сохранили типизацию, хотя сделали это вручную.
В принципе, токеном может быть и обычная строка:
Иерархическое внедрение зависимостей
Я уже упоминал, что Angular2-приложение — это дерево компонентов. И у каждого компонента есть свой роутер и инжектор. Таким образом дерево инжекторов и компонентов параллельны.
Заметьте, в коде сервисов нет нигде упоминания о провайдерах. Мы не можем зарегистрировать какой-то провайдер в рамках какого-нибудь сервиса. Если в сервис внедряется другой сервис, его провайдер регистрируется в каком-то компоненте. Мы не сможем внедрить сервис без компонента. Таким образом, ещё раз подчёркивается компонентный подход всего фреймворка: сервисный слой стал вторичным, на первое место вышли компоненты. И у каждого компонента могут быть свои личные изолированные от других экземпляры сервисов.
Разумеется, ангуляр не создаёт для каждого компонента отдельный инжектор. Это было бы довольно неэффективно. Но в любом случае, каждый компонент имеет свой инжектор, даже если делит его с другим компонентом.
Вот пример того, как работают инжекторы с иерархией:
LogA: LogB:
Означает ли это, что сервисы в Angular2 не являются синглтонами? В конкретном инжекторе не может быть больше 1-го инстанса сервиса. Но так как самих инжекторов может быть несколько, то и разных инстансов одного и того же сервиса во всём приложении может быть больше одного.
Выводы
Ну и несколько советов:
Сервисы и dependency injection
Сервисы
Сервисы в Angular представляют довольно широкий спектр классов, которые выполняют некоторые специфические задачи, например, логгирование, работу с данными и т.д.
В отличие от компонентов и директив сервисы не работают с представлениями, то есть с разметкой html, не оказывают на нее прямого влияния. Они выполняют строго определенную и достаточно узкую задачу.
Стандартные задачи сервисов:
Предоставление данных приложению. Сервис может сам хранить данные в памяти, либо для получения данных может обращаться к какому-нибудь источнику данных, например, к серверу.
Сервис может представлять канал взаимодействия между отдельными компонентами приложения
Сервис может инкапсулировать бизнес-логику, различные вычислительные задачи, задачи по логгированию, которые лучше выносить из компонентов. Тем самым код компонентов будет сосредоточен непосредственно на работе с представлением. Кроме того, тем самым мы также можем решить проблему повторения кода, если нам потребуется выполнить одну и ту же задачу в разных компонентах и классах
Допустим, у нас есть следующая базовая структура проекта:
Определим в этом файле следующий сервис:
В сервисе определен массив данных и методы для работы с ним. В реальном приложении эти данные можно получать с сервера или из какого-либо внешнего хранилища.
Теперь определим код компонента:
Чтобы задействовать сервис в компоненте, его не только надо импортировать:
Но также необходимо его добавить в коллекцию providers компонента:
Все используемые сервисы добавляются в коллекцию providers. Тем самым определяется провайдер сервиса, который будет использоваться для его получения. И после этого мы сможем задействовать встроенный в Angular механизм dependency injection и получить объект сервиса в конструкторе компонента и затем использовать по необходимости:
Для загрузки данных реализуется метод ngOnInit() интерфейса OnInit, который вызывается после конструктора.
И так как мы используем директиву ngModel для работы с элементами формы, то нам надо подключить модуль FormsModule в файле app.module.ts:
В итоге мы сможем и выводить данные и добавлять новые элементы через сервис:
Angular: полное руководство для «Внедрения зависимостей»
Об одной из важнейшей функциональностей фреймворка Angular «Внедрение зависимостей» расскажу я, Александр Желнин, full-stack разработчик, архитектор Департамента информационных технологий Россельхозбанка и автор YouTube-канала.
Мне захотелось представить на конкретных примерах все возможные варианты регистрации зависимостей и их подключения в классах. А также поделиться трудностями, с которыми можно столкнуться, и рассказать об опыте применения «Внедрения зависимостей».
На просторах рунета о «Внедрения зависимостей» написано преступно мало, в основном затрагивается, только «Внедрение сервисов». A «Внедрение зависимостей» — это не только сервисы, но и другие сущности, о которых по какой-то причине не говорят. С помощью «Внедрение зависимостей» вы можете передавать настройки, например, URL к back-end сервису и многое другое. Так же в зависимости от типа сборки можно подменять зависимости с учётом особенностей, например, для тестирования или «параноидального» логирования.
Немного теории по теме
Начнём с такого набора принципов, который называется SOLID. Каждая буква из этого названия — это первая буква соответствующего принципа. Обратим внимание на последний принцип Dependency Inversion «Инверсия зависимостей», который говорит о том, что объект не должен создавать зависимости внутри себя, а должен получать эти зависимости, например, в конструкторе. Ниже приведу примеры без инверсии зависимостей и с инверсией зависимостей. Начнем с примера без инверсии:
В этом примере в классе машина создаёт зависимость на опции машины. У классического подхода без «Инверсии зависимостей» есть недостатки, например, повышается сложность такого класса, потому что класс должен знать, как и откуда получать зависимости.
Рассмотрим пример с «Инверсией зависимостей»:
Принцип «Инверсия зависимостей» даёт ряд неоспоримых преимуществ:
упрощает взаимосвязи класса: класс получает зависимости в конструкторе
юнит-тестирование класса упрощается: тестирование происходит только класса, а не его зависимостей
К минусам при «голом» использовании принципа «Инверсия зависимостей» можно отнести то, что объекты класса могут создаваться множество раз, и писать код для передачи зависимостей — трудозатратно.
Механизм, позволивший автоматизировать процесс создания объектов с учётом зависимостей, называется Dependency Injection «Внедрения зависимостей».
Для понимания: принцип «Инверсии зависимостей» и механизм «Внедрения зависимостей» — это разные понятия и в первом случае — это паттерн, а во втором — реализация, позволяющая нам использовать этот принцип на практике.
В Angular механизм «Внедрение зависимостей» работает по принципу:
Во-первых, создаётся механизм регистрации зависимостей, который называется DI Container
Во-вторых, создание объекта переносится на фабрику классов, называемую Injector
В итоге фабрика создаёт объекты, передавая зависимости, которые получает из контейнера зависимостей.
В качестве зависимостей можем использовать такие типы как:
Сервисы (выделил в отдельный пункт, потому что сервисы часто получают другие сервисы, в качестве зависимостей и т.д.)
Такое разделение условное, и служит лишь примером.
Как же регистрируется зависимости?
Для ответа на этот вопрос необходимо понять, что существуют несколько уровней иерархии для регистрации зависимостей:
Зарегистрировать зависимость можно на любом из уровней, представленном на схеме. Для понимания, каждый элемент схемы — это уровень. В итоге получаем иерархию регистраций зависимостей. При получении зависимости фабрика если не находит зависимость на текущем уровне, переходит вверх по иерархии, пока не найдёт необходимую зависимость.
Пояснения по схеме:
Первый уровень — это уровень платформы. В своей практике я не регистрирую зависимости на этом уровне, в этом нет необходимости, но такая возможность присутствует.
Большинство зависимостей регистрируются в модулях. Общие зависимости регистрируются в корневом модуле, и если его не переименовывали, то он называется AppModule.
Перейдём непосредственно к регистрации зависимостей.
В Angular зависимости можно зарегистрировать 2-мя возможными способами:
для модулей, компонентов и директив, регистрация осуществляется в соответствующем декораторе, в разделе providers
для @Injectable и InjectionToken есть свойство providedIn. В этом случае зависимость регистрирует себя сама.
Давайте подробно рассмотрим оба способа.
Регистрация в providers
В простейшем случае регистрация зависимости, выглядит так:
В более сложном случае необходимо указывать ключ и реализацию экземпляра зависимости:
Для начала разберём какие варианты «ключей» бывают:
Строка
Этот вариант плох тем, что строковый ключ легко могут использовать в подключаемой библиотеке, в результате чего будет конфликт. Настоятельно не рекомендую применять этот вариант на практике, чтобы потом не тратить время на поиск ошибки.
Класс
Если хотим делать несколько реализаций, то в качестве ключа можем использовать класс. Но при одной реализации лучше применять упрощённую регистрацию, которая описана ниже в примере UseClass.
Токен
Использование токена в качестве ключа наиболее предпочтительно. Строковый ключ рекомендую всегда заменять на токен.
С вариантами ключей разобрались, перейдём к вариантам подстановки зависимости. Angular поддерживает такие варианты как useClass, useValue, useFactory, useExisting.
Подробно разберём каждый из этих вариантов:
useClass
Это самый простой вариант, который заключается в том, что для реализации указывается класс.
Пример ниже это упрощённая запись примера выше. И если хотите зарегистрировать сервис без вариантов, то сокращённая запись предпочтительнее.
useValue
В этом варианте подставляем конкретный экземпляр значения, которое может быть любым типом данных.
С помощь регистрации значений часто регистрируют конфигурационные значения. Например, Angular содержит файлы environment, в которых хранятся конфигурационные значения в зависимости от типа сборки, но к этим файлам нет доступа из подключаемых библиотек. В своей практике часто беру значение из environment и регистрирую это значение в контейнере, после чего к конфигурации получает доступ библиотека. В примере ключ сделал строкой исключительно в демонстрационных целях, в реальных проекта используйте токены.
useFactory
Это вариант, в котором функция регистрируется как результат. Функция выполняет роль фабрики, возвращающей значение зависимости.
Вариант useFactory отличается от варианта useValue c функцией тем, что когда возвращается функция в useValue, потом с этой функцией необходимо работать как с функцией, а с фабрикой получаем значение, с которым и работаем, и нет повторных вызовов функции.
Для работы фабрики часто необходимо получать зависимости, поэтому предусмотрен механизм передачи зависимостей в функцию фабрики.
Хочу привести «реальный» пример, которой заключается в том, что необходимо получать настройки, например, с back-end, а потом зарегистрировать эти настройки в качестве зависимости.
В представленном примере хотелось бы обратить внимание на свойство deps, которое осуществляет передачу зависимостей в фабрику.
useExisting
Этот вариант наиболее непонятный для новичка. Суть useExisting заключается в том, что выбирается уже существующая зависимость.
Сразу отвечу на первый же вопрос – почему мы не должны написать код так:
Этот вариант регистрация зависимости создаст нам два экземпляра CarService. Что может доставить много не удобств при отладке, т.к. сервис часто хранит состояние, в результате чего произойдёт так называемый сайд-эффект.
Расскажу почему ещё так важен useExisting
При работе с компонентом экземпляр компонента регистрируется в «Контейнере зависимостей», таким образом легко получить доступ для родительского компонента.
Разработчики часто пишут универсальные директивы, которые можно использовать в разных компонентах. Поэтому необходимо получить доступ к компоненту по универсальному ключу, а дальше работать с базовым интерфейсом компонента. Для этого нужно использовать регистрацию зависимости useExisting c реализацией forwardRef
forwardRef позволяет обратиться к ещё не зарегистрированной зависимости. Из примера понятно, что в коде декоратора сам компонент ещё не был зарегистрирован в контейнере зависимостей.
Такая же реализация помогает решить задачу получения события загрузки компонента. Ссылка на видео: https://youtu.be/097plGqjP0U
Кроме того, самая часто встречающая реализация — это ngModel. Это тема для отдельной статьи.
Регистрация нескольких зависимостей с одинаковым ключом
Так же хотелось бы обратить внимание на дополнительное свойство multi. Приведу пример, который часто может быть необходим:
Если не указывать свойство multi, то в результате бы работал только HttpInterceptorService2. Свойство multi даёт нам возможность, чтобы одна зависимость не переписывала другу, если ключ совпадает, а накапливала зависимости в массиве.
И если получить зависимость по ключу, то в результате будет массив зависимостей.
Регистрация providedIn
Такой способ даёт возможность зависимости зарегистрировать саму себя. Зависимость может зарегистрировать себя либо в виде сервиса, помеченного декоратором @Injectable,либо при определении токена InjectionToken.
Таким способом можно зарегистрировать зависимость на уровнях:
или указать конкретный тип для регистрации, например, компонент или модуль
‘any’ является особым уровнем регистрации. Этот уровень позволяет создавать отдельный экземпляр зависимости для каждого «лениво загружаемого модуля» (lazy-loaded module)
Обязательно при регистрации токена необходимо указать фабрику
Управление доступом «Внедрения зависимостей» занимаются специальные декораторы
@Self()
Этот декоратор будет брать зависимость только этого же компонента/директивы/модуля, в котором требуется получить зависимость.
Если зависимость не будет зарегистрирована в этом же компоненте, то получим ошибку.
@Optional()
Если зависимость OptionsService не найдена, то options === null никаких ошибок сгенерировано не будет. Так же этот декоратор можно применять с любыми другими декораторами уровня доступа.
@SkipSelf()
Этот декоратор пропускает зарегистрированную зависимость у самого компонента и ищет зависимость выше по иерархии.
@Host()
Специально пример с декоратором, чтобы пояснить чем отличается от @Self().
@Host() указывает, что искать нужно у родительского компонента
Хотелось бы добавить из своего опыта то, что специализированные декораторы применяются редко, сам чаще всего применяю декоратор @Optional. Но возможность иметь полный контроль добавляет плюсов в копилку Angular.
В качестве постскриптума. Механизм «Внедрения зависимостей» в Angular достаточно объёмный, но если разобраться — ничего сложного в нём нет. В этой статье показаны все варианты регистрации и внедрения зависимостей, приведены примеры кода, которые надеюсь раскроют принципы «Внедрения зависимостей». Если возникли вопросы — задавайте в комментариях.




