В 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. В результате мы будем получать новое случайное число каждый раз при использовании <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
Комментарии (11)
dopusteam
04.12.2018 09:26Ещё дополню примером
Как правило, этот кейс не имеет смысла
Подключение сервиса в EagerModule может использоваться для инкапсуляции
Если делать отдельный модуль и в нем предоставлять сервис, то в файле этого модуля можно подключить и настроить какие то сторонние библиотеки, которые не хочется подключать в главный модуль.
Т.е. например, у меня есть отдельный сервис для работы с модальными окнами, который является просто обёрткой над сторонним модулем. Сторонний модуль, в свою очередь, позволяет в импорте указать какие то настройки, через forRoot({someKey: someValue}). Если я укажу у своего сервиса providedIn: 'root', то мне придётся сам сторонний модуль подключать и конфигурировать в AppModule.
Если же я сделаю отдельный модуль для работы с модальными окнами, то в нём я смогу подключить стороннюю либу и настроить её корректно, но добавляя её в AppModule
И ещё полезно знать, что есть viewProviders, которые позволяют ограничить область сервиса на шаблон и всех его наследников.klimentRu Автор
04.12.2018 10:13Если делать отдельный модуль и в нем предоставлять сервис, то в файле этого модуля можно подключить и настроить какие то сторонние библиотеки, которые не хочется подключать в главный модуль.
В таком случае рекомендуется использовать для модуля providers: []
Если действительно понадобится ограничить область видимости сервиса, проще воспользоваться старым способом providers:[], так как он точно не приведет к циклическим зависимостям.
И ещё полезно знать, что есть viewProviders, которые позволяют ограничить область сервиса на шаблон и всех его наследников.
Об этом написано в разделах Внедрение в @Сomponent и @Directive и Внедрение сервиса в компонент (providedIn: SomeComponent)dopusteam
04.12.2018 10:46Об этом написано в разделах Внедрение в @Сomponent и @Directive и Внедрение сервиса в компонент (providedIn: SomeComponent)
Component({providers: [Service]}) не равно Component({viewProviders: [Service]})
Первый предоставляет один сервис для всех экземпляров компонента, второй — для каждого отдельный инстансxitt
05.12.2018 19:36+1По поводу Component({providers: [Service]}): angular.io/guide/providers Then each new instance of the UserEditorComponent gets its own cached service instance.
dopusteam
05.12.2018 22:08Ваша правда, нечасто пользуюсь данным функционалом, видимо неправильно отложилось в голове
klimentRu Автор
05.12.2018 22:59Вот интересная статья про внедрение в компоненты и директивы Transclusion, Injection and Procrastination И к каким непредсказуемым последствиям это может привести.
serf
04.12.2018 17:39А с тестами что изменилось? Ведь DI в Ангуляр по большей части для упрощения написания тестов и введен или нет?
klimentRu Автор
04.12.2018 17:54В отличии от C# или Java, где по интерфейсу можно создавать моковые реализации классов для тестов в Angular внедрение зависимостей служит больше для управления сложностью.
С тестами ничего не изменилось.
Если нужно протестировать сервис у которого нет зависимостей или их мало, то можно создать экземпляр через new MyService()
Если нужно тестировать компонент или что-то другое во что внедряется сервис то все так-же создается тестовый модуль через TestBed и в секции providers: [] мокаются все зависимости.
msdosx86
Приятно видеть ангуляр на хабре