синглтон сервис angular что это
Русские Блоги
Исследование проблем синглтона Angular Service
вступление
Когда мы используем Angular Service, у нас есть такой сценарий: состояние переменной является глобально эффективным, и значение состояния может быть изменено в разных компонентах, и каждый компонент может получить переменную состояния в Service. Последнее значение. Для реализации этого сценария одним из наиболее важных моментов является то, что Служба должна быть одноэлементной. Поскольку при наличии нескольких экземпляров значение переменной состояния, видимое каждым Компонентом, несовместимо. В этой статье мы попытаемся изучить, при каких обстоятельствах Angular Service будет генерировать несколько экземпляров и как решить эту проблему.
Официальный документ Angular оСинглтон-сервисПо теме есть хорошее обсуждение. В дополнение к этой статье, эта статья также относится к другим статьям, обсуждающим похожие темы, в сочетании с моим собственным пониманием. Критика и обсуждение приветствуются.
Когда будет несколько экземпляров?
В общем, Angular будет генерировать службу с несколькими экземплярами в двух ситуациях:
Заявить, что общий модуль Сервиса импортируется несколькими другими модулями.
Объявить, что общий модуль Сервиса импортируется другими модулями с отложенной загрузкой.
Эти две ситуации выглядят схожими, но на самом деле принципы, лежащие в их основе, несколько различаются. Давайте рассмотрим их по отдельности.
Вот этот CustomerService Это также может быть необходимо в других модулях приложения, а компоненты других модулей необходимо импортировать перед внедрением. CustomerModule Модуль. Это использование очень распространено в сценариях приложений Angular Router. Когда нам нужно разделить определение маршрутизатора, нам нужно определить разные модули маршрутизации (например, CustomerRoutingModule) и импортировать их в соответствующие модули их соответствующей логики, и эти модули маршрутизации должны импортировать модуль Angular Router. Однако в модуле маршрутизатора есть служба маршрутизатора, которая удовлетворяет первой ситуации, упомянутой выше.Модуль маршрутизатора импортируется несколькими модулями, в результате чего создается несколько экземпляров службы маршрутизатора. Следовательно, Angular сам должен решить эту проблему.
Прежде чем объяснять, как Angular решает эту проблему, давайте рассмотрим детали. Корневой модуль Angular (обычно AppModule) собирается вдоль древовидной структуры модуля во время компиляции, и все поставщики, объявленные модулем по пути, объединяются в один и тот же массив, а для внедрения зависимостей предоставляется инжектор. Другими словами, хотя зависимость модуля Angular является деревом, в конечном итоге она будет объединена в модуль, а структура Injector будет плоской. Если вы хотите узнать об этом больше, вы можете обратиться кЗдесь。
Однако, хотя инжектор такой же, служба по-прежнему является мультиэкземплярной за счет объявления службы в массиве Provider и импорта модуля. В приведенном выше примере при использовании внедрения зависимостей в модуле каждый раз, когда встречается тип CustomerModule Если модуль импортируется, вызывается унифицированный инжектор для создания экземпляра. CustomerService Объект.
Как решить?
Angular предлагает три способа решения этой проблемы:
использовать providedIn Альтернативный синтаксис для регистрации службы в модуле
Реализовать сервис в вызывающем модуле
Определено в модуле forRoot() с forChild() Статический метод
Давайте посмотрим на них по очереди.
использовать providedIn грамматика
Этот синтаксисAngular 6.0После того,Официальный документТакже есть представления. Синтаксис providedIn означает, что эта служба предоставляется в корневом модуле приложения, поэтому нам не нужно объявлять эту службу в массиве предоставления любого модуля. Например:
Реализовать сервис в вызывающем модуле
Это эквивалентно тому, что каждый модуль реализует свою собственную службу, конечно, не будет проблем с несколькими экземплярами. Но когда масштаб программы станет больше, многие Сервисы неизбежно потребуются совместно с модулями, и этот метод не удастся.
Определите статические методы forRoot () и forChild () в модуле
Этот метод может быть вам знаком. Кстати, он используется для регистрации маршрутизации при использовании модуля маршрутизации Angular. Например:
Теперь вы можете понять, почему модуль маршрутизации определяет forRoot Метод: RoutingModule Angular часто упоминается в других модулях, и в этом модуле определены некоторые необходимые службы. Чтобы гарантировать, что эти службы являются одиночными, реализован RoutingModule. forRoot метод.
Так почему forRoot или же forChild Будет ли метод работать? Фактически, эти два имени метода являются только рекомендуемыми идиомами. Метод, который вы определяете в своем собственном модуле, не обязательно может называться этим именем. Если вы соответствуете определенным спецификациям, вы можете реализовать одноэлементную службу для модуля. Посмотрим один forRoot Реализация метода:
Мы видим, что forRoot Статический метод, возвращающийModuleWithProviders Тип интерфейса. Для этого типа требуется один ngModule Поле и один providers Поле массива. Этот метод можно понимать как еще один метод объявления модуля, предоставляемого Angular, через forRoot Этот статический метод получает экземпляр Module.
на forRoot с forChild Разница заключается вПри их внедрении вы в providers Укажите услугу。 forRoot Метод будет уточнен, forChild Нет, поэтому первый может быть вызван только один раз, а второй может быть вызван несколько раз. в случае forRoot Когда метод вызывается несколько раз, все еще возможно сгенерировать службу с несколькими экземплярами.
Конечно, вы также можете предоставить два набора различных сервисов двумя способами.Когда определенная группа должна быть импортирована, соответствующий метод вызывается при импорте модуля.
forRoot Метод используется следующим образом:
Это обеспечит TestModule Сервис, заявленный в синглтоне.
В практических приложениях есть исключение из ситуации, когда модуль загружается лениво через маршрутизацию. Отличительной особенностью модуля с отложенной загрузкой является то, что он имеет собственный инжектор для создания экземпляра службы. Как мы упоминали ранее, Angular объединяет все модули в один модуль во время компиляции, но модули с отложенной загрузкой являются динамическими, и они не будут объединены. Следовательно, если вы ссылаетесь на общий модуль в модуле с отложенной загрузкой, то экземпляр службы действителен только в модуле с отложенной загрузкой, что может привести к созданию службы с несколькими экземплярами.
Поэтому для сценариев с отложенной загрузкой модулей хорошей привычкой является не объявлять Сервис в общем Модуле, а регистрировать Сервис в Модуле более высокого уровня или использовать providedIn грамматика.
Что касается проблемы синглтонов службы, то документы официального веб-сайта Angular были подробно обсуждены.Если вы хотите понять внедрение зависимостей службы и механизм управления синглтонами, вам нужно обратиться к исходному коду.
How do I create a singleton service in Angular 2?
I’ve read that injecting when bootstrapping should have all children share the same instance, but my main and header components (main app includes header component and router-outlet) are each getting a separate instance of my services.
I have a FacebookService which I use to make calls to the facebook javascript api and a UserService that uses the FacebookService. Here’s my bootstrap:
From my logging it looks like the bootstrap call finishes, then I see the FacebookService then UserService being created before the code in each of the constructors runs, the MainAppComponent, the HeaderComponent and the DefaultComponent:
15 Answers 15
Update (Angular 6 +)
The recommended way to create a singleton service has changed. It is now recommended to specify in the @Injectable decorator on the service that it should be provided in the ‘root’. This makes a lot of sense to me and there’s no need to list all the provided services in your modules at all anymore. You just import the services when you need them and they register themselves in the proper place. You can also specify a module so it will only be provided if the module is imported.
Update (Angular 2)
With NgModule, the way to do it now I think is to create a ‘CoreModule’ with your service class in it, and list the service in the module’s providers. Then you import the core module in your main app module which will provide the one instance to any children requesting that class in their constructors:
CoreModule.ts
AppModule.ts
Original Answer
In fact listing your class in ‘providers’ creates a new instance of it, if any parent component already lists it then the children don’t need to, and if they do they will get a new instance.
Jason is completely right! It’s caused by the way dependency injection works. It’s based on hierarchical injectors.
There are several injectors within an Angular2 application:
When Angular2 tries to inject something in the component constructor:
So if you want to have a singleton for the whole application, you need to have the provider defined either at the level of the root injector or the application component injector.
But Angular2 will look at the injector tree from the bottom. This means that the provider at the lowest level will be used and the scope of the associated instance will be this level.
Angular 2 и внедрение зависимостей
Вторая версия ангуляра всё ближе к релизу и всё больше людей начинают интересоваться им. Пока что информации по фреймворку не так много, особенно на русском языке, и всё чаще начинают возникать вопросы по некоторым темам.
Одна из тем, вызывающая много вопросов — внедрение зависимостей. Некоторые люди не сталкивались с подобной технологией. Другие не до конца понимают, как она работает в рамках Angular 2, так как привыкли к другим реализациям, которые бывают в других фреймворках.
А разгадка кроется в том, что DI во втором ангуляре действительно несколько отличается от других, и связано это в первую очередь с общим подходом и философией 2-й версии. Заключается она в том, что сущностями из которых строится всё приложение, являются компоненты. Сервисный слой, роутер, система внедрения зависимостей — вторичны и они имеют смысл только в рамках компонента. Это очень важный момент, который лежит в основе понимания архитектуры нового фреймворка.
Введение
Это пересказ 2-х страниц из оф. документации, касательно внедрению зависимостей в Angular 2: этой и этой.
В статье я буду использовать Typescript. Почему?
Сам фреймворк написан на Typescript, и информации по связке Angular2 + Typescript больше всего.
Код на Typescript с точки зрения синтаксиса — это свежая реализация стандарта ES, дополнительная типизация, и немного вспомогательных фишек. Тем не менее, приложения можно писать и на Javascript, и на Dart. В JS-версии можно не использовать ES6+ синтаксис, однако теряется лаконичность и ясность кода. А если настроить Babel на поддержку свежих фич, то синтаксически всё будет очень похоже на TS-код: классы, аннотации/декораторы, и т.д. Ну только без типов, так что внедрение зависимостей будет выглядеть немного по-другому.
Проблема зависимостей
Представьте, что мы пишем некое абстрактное приложение, разделяя код на небольшие логические кусочки (чтобы не возникло путаницы с терминологией ангуляра, я не буду их называть «компонентами», пускай это будут просто классы-сервисы, в которых содержится бизнес-логика).
Конечно, логики тут нет совсем, но для иллюстрации вполне подойдёт.
Итак, в чём тут проблема? На данный момент Car жёстко зависит от 2-х сервисов, которые вручную создаются в его конструкторе. С точки зрения потребителя сервиса Car это хорошо, ведь зависимость Car сама позаботилась о своих зависимостях. Но, если мы, например, захотим сделать, чтобы в конструктор Engine передавался обязательный параметр, то придётся менять и код самого Car :
Стало быть, создавать экземпляры зависимостей в потребителе не так уж хорошо.
Перепишем код так, чтобы экземпляры зависимостей Car передавались извне:
Уже получше. Код самого сервиса сократился, а сам сервис стал более гибким. Его легче тестировать и конфигурировать:
И с каждым новым компонентом и каждой новой зависимостью всё труднее создавать экземпляры сервисов. Можно конечно сделать фабрику, в которую вынести всю логику по созданию сервиса Car :
На пути к внедрению
Как можно улучшить код? Каждый потребитель знает о том, какие сервисы-зависимости ему нужны. Но чтобы уменьшить связность системы, потребитель не должен создавать их сам. Можно создать класс-синглтон, в котором бы создавались и хранились инстансы всех наших сервисов. В таком классе мы определяем, как нужно создавать необходимые сервисы, а получать их можно, например, по некому ключу. Тогда в сервисах достаточно будет только как-то получить экземпляр такого синглтона, а из него уже получать готовые инстансы зависимостей. Такой паттерн называется ServiceLocator. Это одна из разновидности инверсии контроля. Тогда код выглядел бы примерно так:
С одной стороны, мы избавились от жёсткой связи потребителя и его зависимостей: все сервисы создаются вне потребителей. Но с другой, теперь потребители жёстко связаны с сервис-локатором: каждый потребитель должен знать, где находится инстанс сервис-локатора. Ну и создание сервисов происходит по-прежнему вручную.
Хотелось бы, чтобы можно было просто каким-то образом указать в потребителе его зависимости и переменную, в которую будет помещён экземпляр зависимости, а создание и внедрение самих сервисов сделать автоматическим.
Этим и занимаются DI-фреймворки. Они управляют жизненным циклом внедряемых зависимостей, отслеживают места, где эти зависимости требуются и внедряют их, т.е. передают в потребителя созданный инстанс зависимости, которую потребитель запросил. Из потребителей исчезает жёсткая зависимость от сервис-локатора: теперь с локатором работает DI-фреймворк.
Суть работы примерно такая:
И в зависимости от DI-фреймворка, эти пункты будут выглядеть в коде по-разному.
Ангуляр №1
Для более лучшего понимания устройства второй версии этого фреймворка, в частности, DI, я хотел бы немного описать, как устроена первая его часть.
Жизненный цикл приложения состоит из нескольких этапов. Я хотел бы выделить 2 этапа:
На верхнем уровне находятся модули. Модуль, по-сути, — просто объект, в котором могут регистрироваться и храниться различные части приложения: сервисы, контроллеры, директивы, фильтры. Так же, у модуля могут быть config- и run-колбеки, которые запустятся на соответствующих этапах приложения.
Итак, как же выглядит внедрение зависимостей в первой версии:
Да, тут есть куча всяких нюансов. Например, в первом ангуляре есть аж 5 разных видов сервисов, так что зарегистрировать сервис можно разными способами. А при минификации кода аргументы функции могут измениться, так что лучше использовать другой синтаксис для объявления зависимостей.
Но я не хочу углубляться в дебри первого ангуляра, напишу лишь основные моменты:
Angular 2: новый путь
Вторая версия ангуляра была заявлена как новый фреймворк, написанный с нуля, в котором учли все ошибки первой части. По мере использования 2-й версии у меня сложилось именно такое впечатление. Лишние сущности и концепции исчезли. То, что осталось, стало только лучше и удобнее, а нововведения хорошо вписываются и выглядят логичными.
Первый ангуляр был, по-сути, просто набором полезных приёмов, техник и паттернов, склееных с помощью DI вместе. Но отдельные его части были как-то сами по себе, были слегка разрозненны. Не было единой концепции.
В итоге, структура приложения могла быть совершенно разной. Но вместо свободы обычно это означало смешивание концепций.
Компонентный подход
Поле selector содержит строку, которая будет использоваться в качестве css-селектора для поиска компонента в DOM. Можно передать любой валидный селектор, но чаще всего используют селектор-тэг, не входящий в стандартный набор HTML-тэгов. Таким образом, создаются кастомные тэги.
Поле template содержит строку-шаблон, которым заменится содержимое DOM-элемента, найденного по селектору. Вместо строки с шаблоном можно передать строку с путём до файла-шаблона (только поле будет называться templateUrl ). Подробнее про синтаксис шаблонов можно почитать страницу доков или её русский перевод.
Иерархия компонентов
Что было плохого в первом ангуляре? Там была иерархия скоупов, но сервисный слой был общий для всех. Сервисы настраивались раз и навсегда до запуска приложения, да ещё и были синглтонами.
Ещё были проблемы с роутерами. Оригинальный был довольно скуден, не позволял создавать нормальной иерархии. UI-router был более богат на фичи, позволял использовать несколько view, умел строить иерархию состояний.
Но основная проблема обоих роутеров заключалась в том, что вся эта иерархия путей была абсолютно никак не связана с иерархией скоупов и была крайне не гибкой.
Как же поступили во второй версии? В основе второго ангуляра, как я уже говорил, лежат компоненты. Всё приложение состоит только из компонентов, которые образуют древовидную иерархическую структуру. Корневой компонент загружается с помощью функции bootstrap на HTML-страницу (если используется браузер как целевая платформа). Все остальные компоненты помещаются внутрь корневого и образуют дерево компонентов.
Как же сделать так, чтобы каждый компонент мог бы быть максимально независимым, переиспользуемым и самодостаточным, при этом, избежать дублирования кода?
Чтобы обеспечить компоненту независимость, у него есть метаданные, позволяющие полностью описать всё, что нужно для работы этому компоненту: настройку роутинга, список используемых директив, пайпов и сервисов. Чтобы не быть связанным через сервисный слой, каждый компонент теперь имеет свой роутер и свой инжектор. И они, в свою очередь, так же образуют иерархию, которая всегда связана с иерархией компонентов.
Это и отличает DI в Angular2 от других DI-фреймворков: в ангуляре у приложения нет одного инжектора, у каждого компонента может быть свой инжектор
Внедрение зависимостей в Angular2
Как же выглядит внедрение зависимостей во втором ангуляре? Сервисы теперь внедряются по типу. Внедрение обычно происходит в конструктор потребителя.
Сервисы
Сервис в Angular 2 — это простой класс.
Регистрация сервисов
Чтобы сервис можно было внедрить, сперва нужно зарегистрировать его. Нам не нужно вручную создавать инжектор, ангуляр сам создаёт глобальный инжектор, когда вызывается функция bootstrap :
Вторым аргументом можно передать массив, содержащий провайдеры. Так что один из способов сделать сервис доступным — добавить его класс в список:
Этот код сделает наш сервис доступным для всего приложения. Однако так делать не всегда хорошо. Разработчики фреймворка советуют регистрировать в этом месте только системные провайдеры, и только если они нужны во всей системе. Например, провайдеры роутера, форм и Http-сервисов.
Второй способ зарегистрировать сервис — добавить его в метаданные компонента в поле providers:
Внедрение сервисов в компонент
Самый простой способ внедрить сервис — через конструктор. Так как TypeScript поддерживает типы, то достаточно написать так:
И всё! Если UserService был зарегистрирован, то ангуляр внедрит нужный инстанс в аргумент конструктора.
Внедрение сервисов в сервисы
Так что добавляем Logger в список провайдеров компонента:
Опциональные зависимости
Если внедряемый сервис не обязателен, то нужно добавить аннотацию @Optional :
Провайдеры
Когда мы добавляем класс сервиса в список провайдеров (компонента или в функцию bootstrap), на деле это означает следующее:
На самом деле, когда я говорил, что внедрение происходит по типу, я сказал не всю правду. Токеном, по которому может происходить внедрение может быть не только класс, но об этом немного позже.
Альтернативные провайдеры сервисов
Даже если альтернативный класс имеет какую-то зависимость, которой нет у оригинального сервиса:
Мы всё равно сможем так же просто использовать его, нужно лишь зарегистрировать нужные зависимости:
Алиасы провайдеров
То получится не то, что мы хотели: создадутся 2 экземпляра нового логгера. Один будет использоваться там, где внедряется старый, другой — где внедряется новый логгер. Чтобы создался только 1 инстанс нового логгера, который бы использовался везде, регистрируем провайдер с опцией useExisting :
Провайдеры значений
Иногда проще не создавать отдельный класс, чтобы заменить им провайдер сервиса, а просто использовать готовое значение. Например:
Чтобы использовать уже готовый объект, регистрируем провайдер с опцией useValue :
Провайдер-фабрика / фабричный провайдер
Иногда нужно зарегистрировать провайдер динамически, используя информацию, недоступную с самого начала. Например, эта информация может быть получена из сессии и быть разной от раза к разу. Также предположим, что внедряемый сервис не имеет независимого доступа к этой информации.
В таких случаях используют провайдер-фабрику / фабричный провайдер.
Чтобы использовать фабрику, регистрируем провайдер, передав в поле useFactory наше фабрику, а в поле deps — зависимости этой фабрики:
Токены внедрения зависимостей
Когда мы регистрируем какой-то провайдер в инжекторе, мы ассоциируем этот провайдер с неким токеном, по которому потом получаем зависимость. Обычно, в роли токена выступает класс. Мы бы могли вручную получать инстанс зависимости, используя инжектор напрямую:
Это происходит автоматически, когда мы пишем в конструкторе что-то такое:
Всё потому что ангуляр сам может достать тип аргумента из конструктора и получить по нему зависимость у инжектора.
Неклассовые зависимости
Но что если наша зависимость не является классом? Например, мы хотим внедрить строку, функцию, объект и т.д.
Например, часто надо внедрять объект-конфиг, который будут использовать другие сервисы. Но мы хотим, чтобы этот объект реализовывал определённый интерфейс, чтобы было меньше проблем из-за несоответствия типов:
Мы уже конфигурировали провайдер так, чтобы он возвращал уже созданный объект. Попробуем сделать так же:
Но так сделать не выйдет: интерфейсы не могут быть токенами для инжектора.
Это выглядит странным, ведь в Java или C# мы чаще всего внедряем именно интерфейс (а DI-фреймворк находит нужную его реализацию), а не класс. Но тут такой штуки не выйдет. И это вина не ангуляра, а самого JavaScript. Дело в том, что interface — это фича TypeScript, и существует он только на этапе компиляции. В рантайме нет никаких интерфейсов, так что внедрить интерфейс тайпскрипта мы не сможем.
Решение проблемы
Чтобы внедрить такую зависимость, используем аннотацию @Inject :
В итоге, мы сохранили типизацию, хотя сделали это вручную.
В принципе, токеном может быть и обычная строка:
Иерархическое внедрение зависимостей
Я уже упоминал, что Angular2-приложение — это дерево компонентов. И у каждого компонента есть свой роутер и инжектор. Таким образом дерево инжекторов и компонентов параллельны.
Заметьте, в коде сервисов нет нигде упоминания о провайдерах. Мы не можем зарегистрировать какой-то провайдер в рамках какого-нибудь сервиса. Если в сервис внедряется другой сервис, его провайдер регистрируется в каком-то компоненте. Мы не сможем внедрить сервис без компонента. Таким образом, ещё раз подчёркивается компонентный подход всего фреймворка: сервисный слой стал вторичным, на первое место вышли компоненты. И у каждого компонента могут быть свои личные изолированные от других экземпляры сервисов.
Разумеется, ангуляр не создаёт для каждого компонента отдельный инжектор. Это было бы довольно неэффективно. Но в любом случае, каждый компонент имеет свой инжектор, даже если делит его с другим компонентом.
Вот пример того, как работают инжекторы с иерархией:
LogA: LogB:
Означает ли это, что сервисы в Angular2 не являются синглтонами? В конкретном инжекторе не может быть больше 1-го инстанса сервиса. Но так как самих инжекторов может быть несколько, то и разных инстансов одного и того же сервиса во всём приложении может быть больше одного.
Выводы
Ну и несколько советов:
Angular Singleton Service
Предоставление единого сервиса
В Angular существует два способа сделать сервис одиночным:
Использование предоставлено
Начиная с Angular 6.0, предпочтительным способом создания одноэлементного сервиса является задание в корневом каталоге обеспеченного входа в декораторе @Injectable () службы. Это говорит Angular о предоставлении сервиса в корне приложения.
Массив провайдеров NgModule
В приложениях, созданных с версиями Angular до 6.0, сервисы регистрируются массивами провайдеров NgModule следующим образом:
Если бы этот NgModule был корневым AppModule, UserService был бы единичным и доступным по всему приложению. Хотя вы можете видеть, что это закодировано таким образом, использование свойстваIn в декораторе @Injectable () для самого сервиса предпочтительнее, чем в Angular 6.0, поскольку оно делает ваши сервисы доступными для дерева.
Шаблон forRoot ()
Как правило, вам понадобится только предоставленный для предоставления услуг и forRoot () / forChild () для маршрутизации. Однако понимание того, как работает forRoot (), чтобы убедиться, что служба является одноэлементной, проинформирует вашу разработку на более глубоком уровне.
Если модуль определяет как поставщиков, так и декларации (компоненты, директивы, каналы), загрузка модуля в несколько функциональных модулей дублирует регистрацию службы. Это может привести к нескольким экземплярам службы, и служба больше не будет работать как одиночная.
Есть несколько способов предотвратить это:
Используйте forRoot () для отделения поставщиков от модуля, чтобы вы могли импортировать этот модуль в корневой модуль с поставщиками и дочерними модулями без поставщиков.
Greeting.module.ts
forRoot () и роутер
RouterModule предоставляет сервис Router, а также директивы маршрутизатора, такие как RouterOutlet и routerLink. Модуль корневого приложения импортирует RouterModule, чтобы приложение имело Маршрутизатор, а компоненты корневого приложения могли получить доступ к директивам маршрутизатора. Любые функциональные модули также должны импортировать RouterModule, чтобы их компоненты могли помещать директивы маршрутизатора в свои шаблоны.