Синглтонные сервисы¶
28.02.2022
Синглтон-сервис — это сервис, для которого в приложении существует только один экземпляр.
Пример приложения, использующего синглтон-сервис, описанный на этой странице, приведен в живом примере, демонстрирующем все документированные возможности NgModules.
Предоставление однопользовательского сервиса¶
В Angular существует два способа сделать сервис синглтоном:
- Установить свойство
providedIn
в@Injectable()
в значение"root"
. - Включить сервис в
AppModule
или в модуль, который импортируется толькоAppModule
.
Использование providedIn
¶
Начиная с версии Angular 6.0, предпочтительным способом создания синглтонного сервиса является установка значения providedIn
в root
для декоратора сервиса @Injectable()
. Это указывает Angular на то, что сервис должен быть предоставлен в корне приложения.
1 2 3 4 5 6 |
|
Более подробную информацию о сервисах см. в главе Services учебника Tour of Heroes.
Массив NgModule providers
¶
В приложениях, построенных на базе Angular версий, предшествующих 6.0, сервисы регистрируются в массивах NgModule providers
следующим образом:
1 2 3 4 5 |
|
Если бы этот NgModule был корневым AppModule
, то UserService
был бы синглтоном и был бы доступен во всем приложении. Хотя вы можете увидеть его в таком виде, использование свойства providedIn
декоратора @Injectable()
для самого сервиса предпочтительнее, начиная с версии Angular 6.0, поскольку оно делает ваши сервисы древовидными.
Шаблон forRoot()
¶
Как правило, providedIn
нужен только для предоставления сервисов, а forRoot()
/forChild()
— для маршрутизации. Однако понимание того, как работает forRoot()
для того, чтобы убедиться, что сервис является синглтоном, поможет вам в разработке на более глубоком уровне.
Если в модуле определены как провайдеры, так и декларации (компоненты, директивы, пайпы), то загрузка модуля в несколько функциональных модулей приведет к дублированию регистрации сервиса. Это может привести к появлению нескольких экземпляров сервиса, и сервис перестанет вести себя как синглтон.
Существует несколько способов предотвратить это:
- Использовать синтаксис
providedIn
вместо регистрации сервиса в модуле. - Выделяйте сервисы в отдельный модуль.
- Определите в модуле методы
forRoot()
иforChild()
.
Существует два примера приложений, в которых можно увидеть этот сценарий: более продвинутый NgModules live example, содержащий forRoot()
и forChild()
в модулях маршрутизации и GreetingModule
, и более простой Lazy Loading live example. Вводное пояснение приведено в руководстве Lazy Loading Feature Modules.
Используйте forRoot()
для отделения провайдеров от модуля, чтобы импортировать этот модуль в корневой модуль с providers
и дочерние модули без providers
.
- Создайте в модуле статический метод
forRoot()
. - Поместите провайдеры в метод
forRoot()
.
1 2 3 4 5 6 7 8 |
|
forRoot()
и Router
¶
Модуль RouterModule
предоставляет сервис Router
, а также директивы маршрутизатора, такие как RouterOutlet
и routerLink
. Корневой модуль приложения импортирует RouterModule
, чтобы у приложения был Router
и компоненты корневого приложения могли получить доступ к директивам маршрутизатора. Любые функциональные модули также должны импортировать RouterModule
, чтобы их компоненты могли помещать директивы маршрутизатора в свои шаблоны.
Если бы в RouterModule
не было forRoot()
, то каждый функциональный модуль инстанцировал бы новый экземпляр Router
, что привело бы к поломке приложения, поскольку Router
может быть только один. При использовании метода forRoot()
корневой модуль приложения импортирует RouterModule.forRoot(...)
и получает Router
, а все функциональные модули импортируют RouterModule.forChild(...)
, который не инстанцирует другой Router
.
Если у вас есть модуль, в котором есть и провайдеры, и декларации, вы можете использовать эту технику для их разделения, и вы можете встретить этот паттерн в старых приложениях.
Однако, начиная с версии Angular 6.0, наилучшей практикой предоставления сервисов является использование свойства @Injectable()
providedIn
.
Как работает forRoot()
¶
forRoot()
принимает объект конфигурации сервиса и возвращает ModuleWithProviders, который представляет собой простой объект со следующими свойствами:
Свойства | Детали |
---|---|
ngModule | В данном примере класс GreetingModule |
providers | Настроенные провайдеры |
В live example корневой AppModule
импортирует GreetingModule
и добавляет providers
в провайдеры AppModule
. В частности, Angular накапливает все импортированные провайдеры перед добавлением элементов, перечисленных в @NgModule.providers
. Такая последовательность гарантирует, что все, что вы явно добавите в провайдеры AppModule
, будет иметь приоритет над провайдерами импортированных модулей.
В примере импортируется GreetingModule
и его метод forRoot()
используется один раз, в AppModule
. Такая однократная регистрация позволяет избежать многократного использования.
Можно также добавить метод forRoot()
в GreetingModule
, который настраивает приветствие UserService
.
В следующем примере необязательный, инжектируемый UserServiceConfig
расширяет приветствие UserService
. Если существует UserServiceConfig
, то UserService
устанавливает имя пользователя из этого конфига.
1 2 3 |
|
Вот forRoot()
, который принимает объект UserServiceConfig
:
1 2 3 4 5 6 7 8 |
|
И, наконец, вызвать его в списке imports
модуля AppModule
. В приведенном ниже фрагменте другие части файла опущены. Полный текст файла см. в живом примере или перейдите к следующему разделу этого документа.
1 2 3 4 5 6 |
|
Приложение отображает в качестве пользователя "Мисс Марпл", а не "Шерлока Холмса" по умолчанию.
Не забудьте импортировать GreetingModule
как Javascript-импорт в верхней части файла и не добавляйте его более чем в один список @NgModule
imports
.
Предотвращение повторного импорта GreetingModule
¶
Только корневой AppModule
должен импортировать GreetingModule
. Если лениво загруженный модуль импортирует и его, то приложение может сгенерировать множественные экземпляры сервиса.
Для защиты от повторного импортирования GreetingModule
лениво загружаемым модулем добавьте следующий конструктор GreetingModule
.
1 2 3 4 5 6 |
|
Конструктор указывает Angular на необходимость инжектировать GreetingModule
в себя. Инъекция была бы круговой, если бы Angular искал GreetingModule
в текущем инжекторе, но декоратор @SkipSelf()
означает "искать GreetingModule
в инжекторе-предке, выше меня в иерархии инжекторов".
По умолчанию инжектор выбрасывает ошибку, если не может найти запрашиваемый провайдер. Декоратор @Optional()
означает, что не найти сервис — это нормально. Инжектор возвращает null
, параметр parentModule
равен null, и конструктор завершается без проблем.
Совсем другое дело, если вы неправильно импортируете GreetingModule
в лениво загружаемый модуль, например CustomersModule
.
Angular создает лениво загружаемый модуль со своим собственным инжектором, дочерним по отношению к корневому инжектору. @SkipSelf()
заставляет Angular искать GreetingModule
в родительском инжекторе, которым на этот раз является корневой инжектор. Конечно же, он находит экземпляр, импортированный корневым AppModule
. Теперь parentModule
существует, и конструктор выбрасывает ошибку.
Вот два файла целиком для справки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
Подробнее о NgModules¶
Вам также может быть интересно:
- Sharing Modules, в котором подробно рассматриваются концепции, изложенные на этой странице
- Lazy Loading Modules
- FAQ по NgModule