[Перевод] Angular 6+ полное руководство по внедрению зависимостей. providedIn vs providers:[]
В Angular 6 появился новый улучшенный синтаксис для внедрения зависимостей сервисов в приложение (provideIn). Несмотря на то, что уже вышел Angular 7, эта тема до сих пор остается актуальной. Существует много путаницы в комментариях GitHub, Slack и Stack Overflow, так что давайте подробно разберем эту тему.
В данной статье мы рассмотрим:
- Внедрение зависимостей (dependency injection);
- Старый способ внедрения зависимостей в Angular (providers: []);
- Новый способ внедрения зависимостей в Angular (providedIn: 'root' | SomeModule);
- Сценарии использования provideIn;
- Рекомендации по использованию нового синтаксиса в приложениях;
- Подведем итоги.
Внедрение зависимостей (dependency Injection)
Можете пропустить этот раздел если вы уже имеете представление о DI.
Внедрение зависимостей (DI) — это способ создания объектов, которые зависят от других объектов. Система внедрения зависимостей предоставляет зависимые объекты, когда создает экземпляр класса.
— Документация Angular
Формальные объяснения хороши, но давайте разберем более подробно, что такое внедрение зависимостей.
Все компоненты и сервисы являются классами. Каждый класс имеет специальный метод constructor, при вызове которого создается объект-экземпляр данного класса, использующийся в приложении.
Допустим в одном из наших сервисов имеется следующий код:
constructor(private http: HttpClient)
Если создавать его, не используя механизм внедрения зависимостей, то необходимо добавить HttpClient вручную. Тогда код будет выглядеть следующим образом:
const myService = new MyService(httpClient)
Но откуда в таком случае взять httpClient? Его тоже необходимо создать:
const httpClient = new HttpClient(httpHandler)
Но откуда теперь взять httpHandler? И так далее, пока не будут созданы экземпляры всех необходимых классов. Как мы видим, ручное создание может быть сложным и в процессе могут возникать ошибки.
Механизм внедрения зависимостей Angular делает все это автоматически. Все, что нам нужно сделать, это указать зависимости в конструкторе компонентов, и они будут добавлены без каких-либо усилий с нашей стороны.
Старый способ внедрения зависимостей в Angular (providers: [])
Для запуска приложения Angular должен знать о каждом отдельном объекте, который мы хотим внедрить в компоненты и сервисы. До релиза Angular 6 единственным способом сделать это было указание сервисов в свойстве providers: [] декораторов @NgModule, @Сomponent и @Directive.
Разберем три основных случая использования providers: []:
- В декораторе @NgModule немедленно загружаемого модуля (eager);
- В декораторе @NgModule модуля с отложенной загрузкой (lazy);
- В декораторах @Сomponent и @Directive.
Модули, загружаемые с приложением (Eager)
В данном случае сервис регистрируется в глобальной области видимости как синглтон. Он будет синглтоном даже если включен в providers[] нескольких модулей. Создается единственный экземпляр класса сервиса, который будет зарегистрирован на уровне корня приложения.
Модули с отложенной загрузкой (Lazy)
Экземпляр сервиса, подключенного к lazy модулю, будет создан во время его инициализации. Добавление такого сервиса в компонент eager модуля приведет к ошибке: No provider for MyService! error.
Внедрение в @Сomponent и @Directive
При внедрении в компонент или директиву создается отдельный экземпляр сервиса, который будет доступен в данном компоненте и всех дочерних. В этой ситуации сервис не будет синглтоном, его экземпляр будет создаваться каждый раз при использовании компонента и удаляться вместе с удалением компонента из DOM.
В данном случае RandomService не внедрен на уровень модуля и не является синглтоном,
а зарегистрирован в providers: [] компонента RandomComponent. В результате мы будем получать новое случайное число каждый раз при использовании
Новый способ внедрения зависимостей в Angular (providedIn: 'root' | SomeModule)
В Angular 6 мы получили новый инструмент «Tree-shakable providers» для внедрения зависимостей в приложение, который можно использовать с помощью свойства providedIn декоратора @Injectable.
Можно представить providedIn как внедрение зависимостей в обратном направлении: раньше в модуле описывались сервисы, в которые он будет подключен, теперь в сервисе определяется модуль, к которому его подключать.
Сервис может быть внедрен в корень приложения (providedIn: 'root') или в любой модуль (providedIn: SomeModule). providedIn: 'root' является сокращением для внедрения в AppModule.
Разберем основные сценария использования нового синтаксиса:
- Внедрение в корневой модуль приложения (providedIn: 'root');
- Внедрение в немедленно загружаемый модуль (eager);
- Внедрение в модуль с отложенной загрузкой (lazy).
Внедрение в корневой модуль приложения (providedIn: 'root')
Это самый распространенный вариант внедрения зависимостей. В данном случае сервис будет добавлен в бандл приложение только если он реально используется, т.е. внедрен в компонент или другой сервис.
При использовании нового подхода не будет особой разницы в монолитном SPA приложении, где используются все написанные сервисы, однако providedIn: 'root' будет полезен при написании библиотек.
Раньше все сервисы библиотеки необходимо было добавить в providers:[] её модуля. После импорта библиотеки в приложение в бандл добавлялись все сервисы, даже если использовался только один. В случае с providedIn: 'root' нет необходимости подключать модуль библиотеки. Достаточно просто внедрить сервис в нужный компонент.
Модуль с отложенной загрузкой (lazy) и providedIn: «root»
Что произойдет, если внедрить сервис с providedIn: 'root' в lazy модуль?
Технически 'root' обозначает AppModule, но Angular достаточно умен, чтоб добавить сервис в бандл lazy модуля, если он внедрен только в его компоненты и сервисы. Но есть одна проблема (хотя некоторые люди утверждают, что это фича). Если позже внедрить сервис, используемый только в lazy модуле, в основной модуль, то сервис будет перенесен в основной бандл. В больших приложениях с множеством модулей и сервисов это может привести к проблемам с отслеживанием зависимостей и непредсказуемому поведению.
Будьте внимательны! Внедрение одного сервиса во множестве модулей может привести к скрытым зависимостям, которые сложно понять и невозможно распутать.
К счастью есть способы предотвратить это, и мы рассмотрим их ниже.
Внедрение зависимости в немедленно загружаемый модуль (eager)
Как правило, этот кейс не имеет смысла и вместо него мы можем использовать providedIn: 'root'. Подключение сервиса в EagerModule может использоваться для инкапсуляции и предотвратит внедрение без подключения модуля, но в большинстве случаев такой необходимости нет.
Если действительно понадобится ограничить область видимости сервиса, проще воспользоваться старым способом providers:[], так как он точно не приведет к циклическим зависимостям.
По возможности старайтесь использовать providedIn: 'root' во всех eager модулях.
Примечание. Преимущество модулей с отложенной загрузкой (lazy)
Одной из основных фич Angular является возможность легко разбивать приложение на фрагменты, что дает следующие преимущества:
- Небольшой размер основного бандла приложения, из-за чего приложение загружается и стартует быстрее;
- Модуль с отложенной загрузкой хорошо изолирован и подключается в приложении единожды в свойстве loadChildren соответствующего роута.
Благодаря отложенной загрузке целый модуль с сотней сервисов и компонентов возможно удалить или вынести в отдельное приложение или библиотеку, практически не прилагая усилий.
Еще одним преимуществом изолированности lazy модуля является то, что ошибка, допущенная в нем, не повлияет на остальную часть приложения. Теперь можно спать спокойно даже в день релиза.
Внедрение в модуль с отложенной загрузкой (providedIn: LazyModule)
Внедрение зависимости в определенный модуль не дает использовать сервис в остальных частях приложения. Это позволяет сохранить структуру зависимостей, что особо полезно для больших приложений, в которых беспорядочное внедрение зависимостей может привести к путанице.
Интересный факт: Если lazy сервис внедрить в основную часть приложения, то сборка (даже AOT) пройдет без ошибок, но приложение упадет с ошибкой «No provider for LazyService».
Проблема с циклической зависимостью
Воспроизвести ошибку можно следующим образом:
- Создаем модуль LazyModule;
- Создаем сервис LazyService и подключаем, используя providedIn: LazyModule;
- Создаем компонент LazyComponent и подключаем к LazyModule;
- Добавляем LazyService в конструктор компонента LazyComponent;
- Получаем ошибку с циклической зависимостью.
Схематически это выглядит так: service → module → component → service.
Решить эту проблему можно, создав подмодуль LazyServiceModule, который будет подключен в LazyModule. К подмодулю подключить сервисы.
В данном случае придется создать дополнительный модуль, но это не потребует много усилий и даст следующие плюсы:
- Предотвратит внедрение сервиса в другие модули приложения;
- Сервис будет добавлен в бандл, только если он внедрен в компонент или другой сервис, используемый в модуле.
Внедрение сервиса в компонент (providedIn: SomeComponent)
Существует ли возможность внедрить сервис в @Сomponent или @Directive с использованием нового синтаксиса?
На данный момент нет!
Для создания экземпляра сервиса на каждый компонент все так же необходимо использовать providers: [] в декораторах @Сomponent или @Directive.
Рекомендации по использованию нового синтаксиса в приложениях
Библиотеки
providedIn: 'root' хорошо подходит для создания библиотек. Это действительно удобный способ подключить в основное приложение только непосредственно используемую часть функционала и уменьшить размер конечной сборки.
Одним из практических примеров является библиотека ngx-model, которая была переписана с использованием нового синтаксиса и теперь называется @angular-extensions/model. В новой реализации нет необходимости подключать NgxModelModule в приложение, достаточно просто внедрить ModelFactory в нужный компонент. Подробности реализации можно посмотреть тут.
Модули с отложенной загрузкой (lazy)
Используйте для сервисов отдельный модуль providedIn: LazyServicesModule и подключайте его в LazyModule. Такой подход инкапсулирует сервисы и не даст подключить их в другие модули. Это обозначит границы и поможет создать масштабируемую архитектуру.
По моему опыту случайное внедрение в основной или дополнительный модуль (с использованием providedIn: 'root') может привести к путанице и является не лучшим решением!
providedIn: 'root' тоже будет работать корректно, но при использовании providedIn: LazyServideModule мы получим ошибку «missing provider» при внедрении в другие модули и сможем исправить архитектуру. Перенести сервис в более подходящее место в основной части приложения.
В каких случаях стоит использовать providers: [] ?
В случаях, когда необходимо конфигурировать модуль. Например, подключать сервис только в SomeModule.forRoot (someConfig).
С другой стороны, в такой ситуации можно использовать providedIn: 'root'. Это даст гарантию того, что сервис будет добавлен в приложение только один раз.
Выводы
- Используйте providedIn: 'root' чтобы зарегистрировать сервис как синглтон, доступный во всем приложении.
- Для модуля, входящего в основной бандл используйте providedIn: 'root', а не providedIn: EagerlyImportedModule. В исключительных случаях для инкапсуляции используйте providers:[].
- Создавайте подмодуль с сервисами для ограничения их области видимости providedIn: LazyServiceModule при использовании отложенной загрузки.
- Подключайте модуль LazyServiceModule в LazyModule, чтобы предотвратить появление циклической зависимости.
- Используйте providers: [] в декораторах @Сomponent и @Directive для создания нового экземпляра сервиса на каждый новый экземпляр компонента. Экземпляр сервиса также будет доступен во всех дочерних компонентах.
- Всегда ограничивайте области видимости зависимостей, чтобы улучшить архитектуру и избежать запутанных зависимостей.
Ссылки
Оригинал статьи.
Angular — русскоговорящее сообщество.
Angular Meetups in Russia