Что можно положить в механизм Dependency Injection в Angular?
Почти каждый разработчик на Angular может найти в Dependency Injection решение своей проблемы. Это хорошо было видно в комментариях к моей прошлой статье. Люди рассматривали различные варианты работы с данными из DI, сравнивали их удобство для той или иной ситуации. Это здорово, потому что такой простой инструмент дает нам столько возможностей.
Но несколько человек отписались мне, что им тяжело понять DI и его возможности в Angular. В интернете не так уж много материалов о том, как использовать DI эффективно, и для многих разработчиков он сводится к работе с глобальными сервисами или передачей глобальных данных из корня приложения в компоненты.
Давайте посмотрим на этот механизм в Angular чуть глубже.
Знаете ли вы свои зависимости?
Иногда нелегко понять, сколько зависимостей имеет ваш код.
Например, взгляните на этот псевдокласс и посчитайте, сколько зависимостей он имеет:
import { API_URL } from '../../../env/api-url';
import { Logger } from '../../services/logger';
class PseudoClass {
request() {
fetch(API_URL).then(...);
}
onError(error) {
const logger = new Logger();
logger.log(document.location, error);
}
}
API_URL — импортированные данные из другого файла тоже можно считать зависимостью вашего класса (зависимость от расположения файла).
new Logger () — также импортированные данные из другого файла и пересоздания множества экземпляров класса, когда нам достаточно лишь одного.
document — также браузерное API и завязка на глобальную переменную.
Ну и что же не так?
Например, такой класс тяжело протестировать, так как он зависит от импортированных данных из других файлов и конкретных сущностей в них.
Другая ситуация: document и fetch будут без проблем работать в вашем браузере. Но если однажды вам потребуется перенести приложение в Server Side Rendering, то в nodejs окружении необходимых глобальных переменных может не быть.
Так и что же за DI и зачем он нужен?
Механизм внедрения зависимостей управляет зависимостями внутри приложения. В принципе, для нас, как для Angular-разработчиков, эта система довольно простая. Есть две основные операции: положить что-то в дерево зависимостей или получить что-то из него.
Механизм внедрения зависимостей управляет зависимостями внутри приложения. В принципе, для нас, как для Angular-разработчиков, эта система довольно простая. Есть две основные операции: положить что-то в дерево зависимостей или получить что-то из него.
Если хотите рассмотреть DI с более теоретической стороны, почитайте о принципе инверсии управления. Также можете посмотреть интересные видеоматериалы по теме: серия видео про IoC и DI у Ильи Климова на русском или небольшое видео про IoC на английском.
Вся магия возникает от порядка, в котором мы поставляем и берем зависимости.
Схема работы областей видимости в DI:
Что мы можем положить в DI?
Первая из операций с DI — что-то положить в него. Собственно, для этого Angular позволяет нам прописывать providers-массив в декораторах наших модулей, компонентов или директив. Давайте посмотрим, из чего этот массив может состоять.
Providing класса
Обычно это знает каждый разработчик на Angular. Это тот случай, когда вы добавляете в приложение сервис.
Angular создает экземпляр класса, когда вы запрашиваете его в первый раз. А с Angular 6 мы можем и вовсе не прописывать классы в массив providers, а указать самому классу, в какое место в DI ему встать с providedIn:
providers: [
{
provide: SomeService,
useClass: SomeService
},
// Angular позволяет сократить эту запись как самый частый кейс:
SomeService
]
Providing значения
Также через DI можно поставлять константные значения. Это может быть как простая строка с URL вашего API, так и сложный Observable с данными.
Providing значения обычно реализуется в связке с InjectionToken. Этот объект — ключ для DI-механизма. Сначала вы говорите: «Я хочу получить вот эти данные по такому ключу». А позже приходите к DI и спрашиваете: «Есть ли что-то по этому ключу?»
Ну и частый кейс — проброс глобальных данных из корня приложения.
Лучше посмотреть это сразу в действии, поэтому давайте взглянем на stackblitz с примером:
Итак, в примере мы получили зависимость из DI вместо того, чтобы импортировать ее как константу из другого файла напрямую. И почему нам от этого лучше?
- Мы можем переопределить значение токена на любом уровне дерева DI, никак не меняя компоненты, которые его используют.
- Мы можем мокировать значение токена подходящими данными при тестировании.
- Компонент полностью изолирован и всегда будет работать одинаково, независимо от контекста.
Providing фабрики
На мой взгляд, это самый мощный инструмент в механизме внедрения зависимостей Angular.
Вы можете создать токен, в котором будет результат комбинации и преобразования значений других токенов.
Вот еще один stackbitz с подробным примером создания фабрики со стримом.
Можно найти много кейсов, когда providing фабрики экономит время или делает код более читабельным. Иногда мы внедряем зависимости в компоненты только ради того, чтобы совместить их или преобразовать в совершенно иной формат. В предыдущей статье я рассматривал этот вопрос подробнее и показывал альтернативный подход к решению таких ситуаций.
Providing существующего экземпляра
Не самый частый случай, но этот вариант бывает очень полезным инструментом.
Вы можете положить в токен сущность, которая уже была создана. Хорошо работает в связке с forwardRef.
Посмотрите еще один пример со stackblitz с директивой, которая имплементирует интерфейс и подменяет собой другой токен через useExisting. В этом примере мы хотим переопределить значение токена только в области видимости DI для дочерних компонентов элемента, на котором висит директива. Причем директива может быть любой — главное, что она реализует необходимый интерфейс.
Хитрости с DI-декораторами
DI-декораторы позволяют сделать запросы к DI более гибкими.
Если вы не знаете все четыре декоратора, советую почитать вот эту статью на «Медиуме». Статья на английском, но там очень классные и понятные визуализации по теме.
Не многие также знают, что DI-декораторы можно использовать в массиве deps, который готовит аргументы для фабрики в providers.
providers: [
{
provide: SOME_TOKEN,
/**
* Чтобы фабрика получила декорированное значение, используйте такой
* [new Decorator(), new Decorator(),..., TOKEN]
* синтаксис.
*
* В этом случае вы получите ‘null’, если не будет значения для
* OPTIONAL_TOKEN
*/
deps: [[new Optional(), OPTIONAL_TOKEN]],
useFactory: someTokenFactory
}
]
Фабрика токена
Конструктор InjectionToken принимает два аргумента.
Второй аргумент — объект с конфигурацией токена.
Фабрика токена это функция, которая вызывается в момент, когда кто-то запрашивает этот токен в первый раз. В ней можно посчитать некое стандартное значение для токена или даже обращаться к другим DI-сущностям через функцию inject.
Посмотрите пример реализации функционала стрима нажатия кнопок, но уже на фабрике токена.
Заключение
DI в Angular удивительная тема: на первый взгляд, в нем не так много различных рычагов и инструментов для изучения, но можно писать и говорить часами о тех возможностях и способах применения, которые они нам дают.
Надеюсь, этой статьей мне удалось дать вам фундамент, на основе которого вы сможете придумать собственные решения для упрощения работы с данными в ваших приложениях и библиотеках.