providedIn: 'root': синглтон-сервисы, область видимости

Модуль 4: Node.js, Next.js и Angular
15 уровень , 4 лекция
Открыта

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.

3
Опрос
Создание сервиса, 15 уровень, 4 лекция
Недоступен
Создание сервиса
Создание сервиса
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ