1. Знакомство с providedIn
Когда вы создаёте сервис в Angular, вы обязательно помечаете его декоратором @Injectable(). Но сам по себе этот декоратор — не магическая палочка, которая делает сервис доступным везде. Чтобы Angular знал, где и как создавать экземпляры вашего сервиса, нужно указать, где он будет "предоставлен" (provided).
Раньше (до Angular 6) это делалось через массив providers в модулях (@NgModule.providers). Например:
@NgModule({
providers: [MyService]
})
export class AppModule {}
Но начиная с Angular 6, появился новый, более удобный способ: свойство providedIn в декораторе @Injectable().
@Injectable({
providedIn: 'root'
})
export class MyService {}
И тут начинается самое интересное...
Что означает providedIn: 'root'
Когда вы пишете providedIn: 'root', вы говорите Angular: "Добавь этот сервис в корневой инжектор приложения". Это значит, что экземпляр сервиса создаётся ровно один раз на всё приложение — и все компоненты, которые просят этот сервис через конструктор, получают одну и ту же "чашку кофе". Это и есть паттерн синглтон (singleton).
Аналогия
Представьте себе огромный офис с кучей комнат (компонентов). В коридоре стоит один кулер с водой (сервис с providedIn: 'root'). Все сотрудники (компоненты) пьют из одного и того же кулера. Если кулер вдруг сломается (ну, или его кто-то заменит), все это почувствуют. Если же вы поставите кулеры в каждую комнату (providedIn на уровне компонента), у каждого будет своя вода — и своя судьба.
2. Как это выглядит на практике
Angular CLI по умолчанию добавляет providedIn: 'root' при генерации сервисов. Вот типичный код:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CounterService {
private count = 0;
increment() {
this.count++;
}
get value() {
return this.count;
}
}
Теперь этот сервис доступен в любом компоненте приложения, если вы добавите его в конструктор:
import { Component } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-counter',
template: `
<button (click)="inc()">+</button>
<span>{{ counter.value }}</span>
`
})
export class CounterComponent {
constructor(public counter: CounterService) {}
inc() {
this.counter.increment();
}
}
Если вы создадите второй компонент и тоже внедрите туда CounterService, оба компонента будут видеть одно и то же значение счётчика. Вот он, синглтон!
3. Полезные нюансы
Как работает область видимости сервисов
В Angular есть несколько уровней, где можно "провайдить" (предоставлять) сервис:
- 'root' — корневой инжектор (один экземпляр на всё приложение).
- 'platform' — ещё более глобально (редко используется).
- Модуль (AppModule, FeatureModule) — экземпляр на каждый модуль (или общий, если модуль импортируется один раз).
- Компонент — экземпляр на каждый компонент или его поддерево.
providedIn: 'root' — это самый частый и рекомендуемый вариант для большинства бизнес-сервисов и хранилищ состояния.
Почему providedIn: 'root' — это хорошо
- Простота: не надо вручную добавлять сервис в массив providers в модуле.
- Только один экземпляр: Angular гарантирует, что сервис создаётся ровно один раз, и вы не получите неожиданных дубликатов.
- Tree-shaking: если сервис нигде не используется, Angular (и сборщик) вообще не включит его в итоговый бандл — экономия места!
- Явная область видимости: сразу видно, где сервис будет доступен.
Когда не стоит использовать providedIn: 'root'
Иногда вам нужно, чтобы сервис был не глобальным, а, например, создавался заново для каждого "островка" приложения (например, для каждого лениво загружаемого модуля или даже для каждого экземпляра компонента). В таком случае вы можете:
- Указать providedIn на уровень модуля или компонента (например, providedIn: SomeModule).
- Добавить сервис в массив providers компонента:
В этом случае каждый экземпляр компонента получит свой собственный экземпляр сервиса.@Component({ selector: 'app-sandbox', template: '...', providers: [SandboxService] }) export class SandboxComponent {}
Как проверить, что сервис — синглтон?
Давайте добавим в наш сервис небольшой отладочный лог:
@Injectable({
providedIn: 'root'
})
export class CounterService {
private static instanceCount = 0;
constructor() {
CounterService.instanceCount++;
console.log('CounterService создан, всего экземпляров:', CounterService.instanceCount);
}
}
Теперь, если вы создадите несколько компонентов с этим сервисом, в консоли появится только одна строка:
CounterService создан, всего экземпляров: 1
— это и есть синглтон.
Как это связано с областью видимости и временем жизни
- Область видимости: где сервис "виден" и используется (глобально, в модуле, в компоненте).
- Время жизни: когда создаётся и когда уничтожается экземпляр сервиса.
С сервисом providedIn: 'root' всё просто: он создаётся при первом запросе к нему (лениво), и живёт до конца жизни приложения. То есть, если вы не используете сервис — он даже не создаётся (экономия памяти).
Взаимодействие с ленивыми модулями
Иногда приложение делится на ленивые (lazy-loaded) модули. Если сервис провайдится в 'root', он всё равно будет один на всё приложение, даже если используется только в ленивом модуле. Если вы хотите отдельные экземпляры для каждого ленивого модуля — провайдьте сервис на уровне этого модуля.
4. Типичные ошибки при использовании providedIn: 'root'
Ошибка №1: Ожидание разных экземпляров в разных компонентах.
Если вы думаете, что получите "новый" сервис в каждом компоненте, но используете providedIn: 'root', то оба компонента будут работать с одним и тем же объектом. Это может привести к неожиданному "перетиранию" состояния.
Ошибка №2: Дублирование сервисов в providers и providedIn.
Если вы укажете сервис и в providedIn: 'root', и в массиве providers компонента или модуля, Angular создаст отдельный экземпляр для этого компонента/модуля. Это может привести к путанице, когда в одном месте у вас глобальный сервис, а в другом — локальный.
Ошибка №3: Неиспользуемый сервис попал в бандл.
Angular умеет исключать неиспользуемые сервисы с providedIn: 'root' из финального бандла (tree-shaking). Но если вы добавили сервис в providers модуля или компонента, он попадёт в бандл всегда, даже если не используется.
Ошибка №4: Попытка использовать сервис до инициализации приложения.
Если вы как-то вызываете сервис "до" старта Angular-приложения (например, в глобальном скрипте), он ещё не создан. Сервисы с providedIn: 'root' живут только в рамках Angular DI.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ