- PVSM.RU - https://www.pvsm.ru -

Angular 6+ полное руководство по внедрению зависимостей.  providedIn vs providers:[]

image

В Angular 6 появился новый улучшенный синтаксис для внедрения зависимостей сервисов в приложение (provideIn). Несмотря на то, что уже вышел Angular 7, эта тема до сих пор остается актуальной. Существует много путаницы в комментариях GitHub, Slack и Stack Overflow, так что давайте подробно разберем эту тему.

В данной статье мы рассмотрим:

  1. Внедрение зависимостей (dependency injection);
  2. Старый способ внедрения зависимостей в Angular (providers: []);
  3. Новый способ внедрения зависимостей в Angular (providedIn: 'root' | SomeModule);
  4. Сценарии использования provideIn;
  5. Рекомендации по использованию нового синтаксиса в приложениях;
  6. Подведем итоги.

Внедрение зависимостей (dependency Injection)

Можете пропустить этот раздел если вы уже имеете представление о DI.

Внедрение зависимостей (DI) — это способ создания объектов, которые зависят от других объектов. Система внедрения зависимостей предоставляет зависимые объекты, когда создает экземпляр класса.

Документация Angular [1]

Формальные объяснения хороши, но давайте разберем более подробно, что такое внедрение зависимостей.

Все компоненты и сервисы являются классами. Каждый класс имеет специальный метод 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.

image

Разберем три основных случая использования providers: []:

  1. В декораторе @NgModule немедленно загружаемого модуля(eager);
  2. В декораторе @NgModule модуля с отложенной загрузкой(lazy);
  3. В декораторах @Сomponent и @Directive.

Модули, загружаемые с приложением (Eager)

В данном случае сервис регистрируется в глобальной области видимости как синглтон. Он будет синглтоном даже если включен в providers[] нескольких модулей. Создается единственный экземпляр класса сервиса, который будет зарегистрирован на уровне корня приложения.

Модули с отложенной загрузкой (Lazy)

Экземпляр сервиса, подключенного к lazy модулю, будет создан во время его инициализации. Добавление такого сервиса в компонент eager модуля приведет к ошибке: No provider for MyService! error.

Внедрение в @Сomponent и @Directive

При внедрении в компонент или директиву создается отдельный экземпляр сервиса, который будет доступен в данном компоненте и всех дочерних. В этой ситуации сервис не будет синглтоном, его экземпляр будет создаваться каждый раз при использовании компонента и удаляться вместе с удалением компонента из DOM.

image

В данном случае 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.

image

Разберем основные сценария использования нового синтаксиса:

  1. Внедрение в корневой модуль приложения (providedIn: 'root');
  2. Внедрение в немедленно загружаемый модуль(eager);
  3. Внедрение в модуль с отложенной загрузкой(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 является возможность легко разбивать приложение на фрагменты, что дает следующие преимущества:

  1. Небольшой размер основного бандла приложения, из-за чего приложение загружается и стартует быстрее;
  2. Модуль с отложенной загрузкой хорошо изолирован и подключается в приложении единожды в свойстве loadChildren соответствующего роута.

Благодаря отложенной загрузке целый модуль с сотней сервисов и компонентов возможно удалить или вынести в отдельное приложение или библиотеку, практически не прилагая усилий.

Еще одним преимуществом изолированности lazy модуля является то, что ошибка, допущенная в нем, не повлияет на остальную часть приложения. Теперь можно спать спокойно даже в день релиза.

Внедрение в модуль с отложенной загрузкой(providedIn: LazyModule)

image

Внедрение зависимости в определенный модуль не дает использовать сервис в остальных частях приложения. Это позволяет сохранить структуру зависимостей, что особо полезно для больших приложений, в которых беспорядочное внедрение зависимостей может привести к путанице.

Интересный факт: Если lazy сервис внедрить в основную часть приложения, то сборка (даже AOT) пройдет без ошибок, но приложение упадет с ошибкой «No provider for LazyService».

Проблема с циклической зависимостью

image

Воспроизвести ошибку можно следующим образом:

  1. Создаем модуль LazyModule;
  2. Создаем сервис LazyService и подключаем, используя providedIn: LazyModule;
  3. Создаем компонент LazyComponent и подключаем к LazyModule;
  4. Добавляем LazyService в конструктор компонента LazyComponent;
  5. Получаем ошибку с циклической зависимостью.

Схематически это выглядит так: service -> module -> component -> service.

Решить эту проблему можно, создав подмодуль LazyServiceModule, который будет подключен в LazyModule. К подмодулю подключить сервисы.
image

В данном случае придется создать дополнительный модуль, но это не потребует много усилий и даст следующие плюсы:

  1. Предотвратит внедрение сервиса в другие модули приложения;
  2. Сервис будет добавлен в бандл, только если он внедрен в компонент или другой сервис, используемый в модуле.

Внедрение сервиса в компонент (providedIn: SomeComponent)

Существует ли возможность внедрить сервис в @Сomponent или @Directive с использованием нового синтаксиса?

На данный момент нет!

Для создания экземпляра сервиса на каждый компонент все так же необходимо использовать providers: [] в декораторах @Сomponent или @Directive.

image

Рекомендации по использованию нового синтаксиса в приложениях

Библиотеки

providedIn: 'root' хорошо подходит для создания библиотек. Это действительно удобный способ подключить в основное приложение только непосредственно используемую часть функционала и уменьшить размер конечной сборки.

Одним из практических примеров является библиотека ngx-model [2], которая была переписана с использованием нового синтаксиса и теперь называется @angular-extensions/model [3]. В новой реализации нет необходимости подключать NgxModelModule в приложение, достаточно просто внедрить ModelFactory в нужный компонент. Подробности реализации можно посмотреть тут [4].

Модули с отложенной загрузкой(lazy)

Используйте для сервисов отдельный модуль providedIn: LazyServicesModule и подключайте его в LazyModule. Такой подход инкапсулирует сервисы и не даст подключить их в другие модули. Это обозначит границы и поможет создать масштабируемую архитектуру.

По моему опыту случайное внедрение в основной или дополнительный модуль (с использованием providedIn: 'root') может привести к путанице и является не лучшим решением!

providedIn: 'root' тоже будет работать корректно, но при использовании providedIn: LazyServideModule мы получим ошибку «missing provider» при внедрении в другие модули и сможем исправить архитектуру. Перенести сервис в более подходящее место в основной части приложения.

В каких случаях стоит использовать providers: [] ?

В случаях, когда необходимо конфигурировать модуль. Например, подключать сервис только в SomeModule.forRoot(someConfig).

image

С другой стороны, в такой ситуации можно использовать providedIn: 'root'. Это даст гарантию того, что сервис будет добавлен в приложение только один раз.

Выводы

  1. Используйте providedIn: 'root' чтобы зарегистрировать сервис как синглтон, доступный во всем приложении.
  2. Для модуля, входящего в основной бандл используйте providedIn: 'root', а не providedIn: EagerlyImportedModule. В исключительных случаях для инкапсуляции используйте providers:[].
  3. Создавайте подмодуль с сервисами для ограничения их области видимости providedIn: LazyServiceModule при использовании отложенной загрузки.
  4. Подключайте модуль LazyServiceModule в LazyModule, чтобы предотвратить появление циклической зависимости.
  5. Используйте providers: [] в декораторах @Сomponent и @Directive для создания нового экземпляра сервиса на каждый новый экземпляр компонента. Экземпляр сервиса также будет доступен во всех дочерних компонентах.
  6. Всегда ограничивайте области видимости зависимостей, чтобы улучшить архитектуру и избежать запутанных зависимостей.

Ссылки

Оригинал статьи. [5]
Angular — русскоговорящее сообщество. [6]
Angular Meetups in Russia [7]

Автор: klimentRu

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/301045

Ссылки в тексте:

[1] Документация Angular: https://angular.io/guide/dependency-injection

[2] ngx-model: https://www.npmjs.com/package/ngx-model

[3] @angular-extensions/model: https://www.npmjs.com/package/@angular-extensions/model

[4] тут: https://github.com/angular-extensions/model/blob/master/lib/model/model.ts#L40

[5] Оригинал статьи.: https://medium.com/@tomastrajan/total-guide-to-angular-6-dependency-injection-providedin-vs-providers-85b7a347b59f

[6] Angular — русскоговорящее сообщество.: https://t.me/angular_ru

[7] Angular Meetups in Russia: https://github.com/Angular-RU/angular-russia-meetups

[8] Источник: https://habr.com/post/429342/?utm_campaign=429342