[Перевод] 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


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

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

Новый способ внедрения зависимостей в 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, которая была переписана с использованием нового синтаксиса и теперь называется @angular-extensions/model. В новой реализации нет необходимости подключать NgxModelModule в приложение, достаточно просто внедрить ModelFactory в нужный компонент. Подробности реализации можно посмотреть тут.

Модули с отложенной загрузкой (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. Всегда ограничивайте области видимости зависимостей, чтобы улучшить архитектуру и избежать запутанных зависимостей.


Ссылки


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

© Habrahabr.ru