От автора: сервисы являются одним из основных блоков каждого приложения. В Angular сервис — это всего лишь класс TypeScript с или даже без декоратора @Injectable.
Чтобы создать сервис, нам нужно создать класс
1 |
export class VoteService {} |
И зарегистрировать его в массиве провайдеров @NgModule:
1 2 3 4 5 6 7 8 |
import {VoteService} from './vote.service'; ... @NgModule({ imports: [ BrowserModule], declarations: [ AppComponent], bootstrap: [ AppComponent], providers: [VoteService] }) |
Второй способ (более предпочтительный в Angular 6) — использовать декоратор @Injectable и указать свойство @Injectable:
1 2 3 4 5 |
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class VoteService { } |
«root» означает, что мы хотим предоставить услугу на корневом уровне (AppModule)
Когда вы предоставляете услугу на корневом уровне, Angular создает единый, общий экземпляр службы и вводит в любой класс, который его запрашивает. Регистрация провайдера в метаданных @Injectable также позволяет Angular оптимизировать приложение, удалив службу, если она не будет использоваться в конце концов.
Итак, здесь все становится интереснее. Когда вы добавляете поставщика услуг в корневой модуль (корневой инжектор), он доступен для всего приложения. Это означает, что если у вас есть модуль функций с сервисом в провайдерах, и эта услуга также предоставляется в корневом модуле, в этом случае оба модуля будут работать с одним и тем же экземпляром службы (singleton pattern). Поэтому имейте в виду, когда вы добавляете сервис в корневой модуль, который доступен для всего приложения, и все компоненты / директивы имеют доступ к одному экземпляру службы, если только модуль функций не ленив.
Итак, давайте посмотрим на это с помощью кода. У нас есть VoteService который предоставляется в корневом модуле ( AppModule ) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class VoteService { votes: number = 10; constructor() { } getVotes() { return this.votes; } setVotes(vote: number) { this.votes = vote; } } |
Итак, у нас есть простой сервис со свойством votes и двумя методами. Один для получения значения этого свойства, а другой для установки нового значения. Начальное значение равно 10.
Теперь давайте добавим наш сервис в app.component и покажем значение votes:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Component({ selector: 'my-app', template: `app component - {{votes}}` }) export class AppComponent { votes: any; constructor(private vt: VoteService) {} ngOnInit() { this.votes = this.vt.getVotes() this.vt.setVotes(25); } } |
Здесь мы вводим нашу службу в конструктор, присваиваем значение голосов от службы переменной компонента и устанавливаем новое значение. Пока в браузере мы увидим это:
1 |
app component - 10 |
10 — это значение, которое было первоначально задано. Теперь давайте создадим функциональный модуль (не ленивый), а также предоставим VoteService:
1 2 3 4 5 6 7 |
@NgModule({ imports: [CommonModule], declarations: [FeatureComponent], exports: [FeatureComponent], providers: [VoteService] }) export class FeatureModule { } |
Этот FeatureModule импортируется в корневой модуль (AppModule). Таким образом, в FeatureComponent мы также можем вводить VoteService и получать значение votes:
1 2 3 4 5 6 7 8 9 10 11 |
@Component({ selector: 'app-fc', template: `{{votes}}` }) export class FeatureComponent implements OnInit { votes: number; constructor(private vt: VoteService) { } ngOnInit() { this.votes = this.vt.getVotes() } } |
Теперь давайте модифицируем шаблон AppComponent, чтобы отображать FeatureComponent:
1 2 3 |
template: `app component - {{votes}} <br> feature component - <app-fac></app-fac>` |
В результате мы увидим
1 2 |
app component - 10 feature component - 25 |
Почему так? Причина в том, что и компонент приложения, и компонент компонента работают с одним и тем же экземпляром VoteService. Посмотрим, как это работает.
Наши модули (как AppModule, так и FeatureModule) имеют VoteService в своих провайдерах
Поскольку мы импортируем FeatureModule в AppModule, и у обоих есть провайдер с тем же токеном (тот же сервис), выигрывает AppModule. Это связано с тем, что оба поставщика добавляются к одному и тому же инжектору.
Angular использует систему аинжекторов. Когда корневой модуль загружается при запуске приложения, все поставщики из всех импортированных модулей добавляются в корневой инжектор, поэтому они доступны во всем приложении.
Когда мы импортируем два модуля, которые предоставляют одну и ту же услугу, второй модуль всегда выигрывает, потому что он был добавлен последним.
В app.component, когда мы VoteService Angular начинает поиск провайдеров внутри этого компонента, затем он поднимается по иерархии, пока не найдет его в корневом модуле (корневой инжектор). После этого Angular проверяет, есть ли экземпляр или нет. Если новый экземпляр не будет создан и возвращен app.component. Вот почему, когда в VoteService мы VoteService мы фактически используем уже созданный экземпляр класса, поэтому мы получаем 25 голосов вместо начальных 10.
А ленивые модули?
Все становится еще интереснее, когда мы используем ленивые модули. Ленивые модули — это модули, которые используют ленивую загрузку. Это означает, что вы загружаете модуль только тогда, когда вам действительно нужно. Например, на вашем сайте есть страница о «новых продуктах», но пользователи редко посещают эту страницу. Вы можете разделить логику своего приложения, сделать ленивый модуль, который будет отвечать за эту страницу, и с тем, что ваше приложение запустится еще быстрее, потому что один модуль не будет загружен, пока кто-то не запустит эту страницу.
Чтобы создать ленивый модуль, нам нужно использовать маршрутизацию. Итак, давайте настроим маршруты приложений в AppModule:
1 2 3 4 |
RouterModule.forRoot([{ path: 'lazy', loadChildren : './lm/lazy.module#LazyModule' }]) |
Здесь мы говорим, будет ли URL загружать /lazy этот модуль, и мы предоставим относительный путь нашего ленивого модуля. Не забудьте создать этот модуль. Вы можете использовать CLI для генерации модуля ng generate module lazy. Мы не импортируем ленивые модули в корневой модуль.
Нам также нужно настроить маршруты в LazyModule. Таким образом, внутри массива импорта LazyModule мы можем сделать это:
1 2 3 |
RouterModule.forChild([{ path: '', component: LazyComponent }]) |
В качестве пути мы используем пустую строку, потому что в AppModule мы уже предоставили маршрут. Также обратите внимание, что вместо forRoot мы используем forChild.
Вот и все, теперь у нас есть ленивый модуль и маршрут для активации этого модуля. Давайте добавим несколько кнопок, которые изменят маршрут.
Измените шаблон app.component:
1 2 3 4 5 6 7 8 9 10 |
@Component({ selector: 'my-app', template: `app component - {{votes}}<br> <button routerLink='lazy'>lazy load module</button> <button (click)="refresh()">refresh</button> <router-outlet></router-outlet> ` }) ... |
Мы по-прежнему показываем количество votes от нашего сервиса, но мы также добавили 2 кнопки, одну для изменения маршрута и загрузки нашего ленивого модуля, а вторую для обновления свойства votes.
1 2 3 |
refresh() { this.votes = this.vt.getVotes() } |
Наконец, давайте создадим шаблон нашего ленивого компонента:
1 2 3 4 |
... template: `<br>lazy component - {{votes}} <button routerLink='/'>back to app component</button>` ... |
Поэтому в нашем LazyModule мы также предоставляем VoteService. Услуга вводится в LazyComponent, поэтому мы можем создать переменную votes и использовать ее для отображения количества голосов от VoteService. Вот что будет.
Первоначально значение votes равно 10. Когда мы лениво загружаем модуль, значение равно 10, а не 25. Когда мы обновляемся, значение votes изменяется только для app.component, а для lazy.component оно остается 10. Теперь давайте посмотрим более детально:
У нас есть два модуля (AppModule и LazyModule), которые предоставляют одну и ту же услугу.
Когда приложение запускает корневой инжектор, создается со всеми сервисами из всех модулей с нетерпением. Однако, поскольку наш модуль ленив, Angular не знает о существовании нашего LazyModule. Это означает, что любая служба, указанная в массиве поставщиков нашего LazyModule, недоступна, потому что инжектор корня не знает о LazyModule … пока. Когда мы активируем маршрут, будет загружен ленивый модуль и будет создан новый инжектор. Представьте дерево инжекторов, сверху есть инжектор корня, и для каждого ленивого модуля будет создан новый дочерний инжектор.
Все услуги от корневого инжектора будут добавлены к инжектору ребенка. Если корневой инжектор и дочерний инжектор предоставляют одну и ту же услугу, Angular предпочитает экземпляры службы из детского инжектора. Поэтому каждый ленивый компонент получает локальный экземпляр службы, а не экземпляр в корневом инжекторе приложения. Вот почему, когда мы обновили состояние, изменились только votes app.component, так как lazy.component работает с другим экземпляром службы.
Так быстро повторим
Если у нас есть несколько функциональных модулей, которые предоставляют одну и ту же услугу, к корневому инжектору будет добавлен только сервис последнего модуля
Если у нас есть несколько функциональных модулей, которые предоставляют одну и ту же услугу, а также этот сервис предоставляется в AppModule (корневой модуль), к корневому инжектору будет добавлен только экземпляр, созданный из AppModule. Поэтому даже компоненты FeatureModules будут использовать экземпляр службы из AppModule
Если у нас есть Lazy Module, который не предоставляет услугу, он будет использовать экземпляр, созданный из корневого инжектора (AppModule)
Но если Lazy Module предоставил услугу, компоненты этого модуля будут использовать локальный экземпляр этой службы (а не экземпляр из корневого инжектора)
Ограничение области видимости провайдера с помощью компонентов
Еще один способ ограничить область видимости провайдера — предоставить услугу в массиве поставщиков внутри декоратора @Component. Поставщики компонентов и поставщики NgModule независимы друг от друга. Предоставление услуги в компоненте ограничивает службу только этим компонентом и компонентами внутри этого компонента (дерева компонентов):
1 2 3 4 |
@Component({ ... providers: [VoteService] }) |
Спасибо за прочтение. Надеюсь, теперь это не так сложно. Полный код доступен здесь.
Автор: Jack Stepanyan
Источник: //itnext.io/
Редакция: Команда webformyself.