В Angular 6 появился новый улучшенный синтаксис для внедрения зависимостей сервисов в приложение (provideIn). Несмотря на то, что уже вышел Angular 7, эта тема до сих пор остается актуальной. Существует много путаницы в комментариях GitHub, Slack и Stack Overflow, так что давайте подробно разберем эту тему.
В данной статье мы рассмотрим:
- Внедрение зависимостей (dependency injection);
- Старый способ внедрения зависимостей в Angular (providers: []);
- Новый способ внедрения зависимостей в Angular (providedIn: 'root' | SomeModule);
- Сценарии использования provideIn;
- Рекомендации по использованию нового синтаксиса в приложениях;
- Подведем итоги.
Внедрение зависимостей (dependency Injection)
Можете пропустить этот раздел если вы уже имеете представление о DI.
Внедрение зависимостей (DI) — это способ создания объектов, которые зависят от других объектов. Система внедрения зависимостей предоставляет зависимые объекты, когда создает экземпляр класса.
Формальные объяснения хороши, но давайте разберем более подробно, что такое внедрение зависимостей.
Все компоненты и сервисы являются классами. Каждый класс имеет специальный метод 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. В результате мы будем получать новое случайное число каждый раз при использовании <randоm></randоm>.
Новый способ внедрения зависимостей в 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
Автор: klimentRu