1. Введение
Если вы уже привыкли к тому, что сервисы — это такие себе "глобальные менеджеры", которых можно инжектировать в любой компонент, то пора узнать: Angular позволяет ограничить область видимости сервиса! Это похоже на то, как если бы у вас был личный ассистент только для конкретной комнаты, а не на весь офис. Иногда это именно то, что нужно для изоляции данных, тестирования, создания независимых экземпляров или просто ради порядка.
В Angular сервисы могут быть предоставлены (provided) на трёх уровнях:
- На уровне всего приложения — через providedIn: 'root' или массив providers в AppModule.
- На уровне модуля — через массив providers в конкретном NgModule.
- На уровне компонента — через массив providers в декораторе компонента.
Каждый способ влияет на область видимости и жизненный цикл сервиса.
Providers на уровне приложения
Это самый частый и "безопасный" способ: сервис объявляется с providedIn: 'root' (или в массиве providers у AppModule). Такой сервис существует в единственном экземпляре на всё приложение до полного его завершения. Любой компонент или сервис, который его инжектирует, получит одну и ту же "копию" (экземпляр).
@Injectable({
providedIn: 'root'
})
export class GlobalLoggerService {
log(message: string) {
console.log('[GLOBAL]', message);
}
}
Это уже знакомо, поэтому долго останавливаться не будем. Давайте двигаться дальше!
2. Providers на уровне модуля
Как это делается?
Angular позволяет объявить сервис в массиве providers любого NgModule (например, в feature-модуле):
@NgModule({
declarations: [FeatureComponent],
providers: [FeatureService], // <-- вот здесь!
imports: [CommonModule]
})
export class FeatureModule {}
Как это работает?
Все компоненты, директивы и пайпы, объявленные в этом модуле, а также их потомки, будут получать один и тот же экземпляр этого сервиса. Но! Если вы инжектируете этот сервис вне модуля — например, в другом модуле или компоненте, который не включён в данный модуль — экземпляр будет другой (или не будет вовсе, если сервис нигде больше не предоставлен).
Аналогия
Это как если бы у каждого отдела в компании был свой "отделочный" принтер, но в других отделах его нет. Все сотрудники отдела печатают на одном принтере, но другие отделы не могут им воспользоваться.
Пример
// feature.service.ts
@Injectable()
export class FeatureService {
private counter = 0;
inc() { this.counter++; }
get value() { return this.counter; }
}
// feature.module.ts
@NgModule({
declarations: [FeatureComponent],
providers: [FeatureService], // <-- тут!
imports: [CommonModule]
})
export class FeatureModule {}
Все компоненты внутри FeatureModule, которые инжектируют FeatureService, будут работать с одним и тем же счетчиком.
Когда это бывает полезно?
- Feature-модули с изолированным состоянием: Например, если у вас несколько независимых разделов приложения, и каждый должен иметь свой сервис для управления состоянием.
- Тестирование: Модули можно тестировать независимо друг от друга.
- Lazy-loading: При ленивой загрузке модуля Angular создаёт отдельный инжектор, и сервисы, предоставленные на уровне этого модуля, будут жить только в этом лениво загружаемом куске приложения.
3. Providers на уровне компонента
Как это делается?
В декораторе компонента можно указать массив providers:
@Component({
selector: 'app-counter',
template: `<button (click)="inc()">{{service.value}}</button>`,
providers: [CounterService] // <-- здесь!
})
export class CounterComponent {
constructor(public service: CounterService) {}
inc() { this.service.inc(); }
}
Как это работает?
Каждый экземпляр компонента получает свой собственный экземпляр сервиса. Даже если компонент используется несколько раз на странице, у каждого будет свой сервис!
Аналогия
Это как если бы у каждого сотрудника компании был свой персональный термос для кофе. Каждый может налить себе кофе сколько хочет, не мешая остальным.
Пример
@Injectable()
export class CounterService {
private counter = 0;
inc() { this.counter++; }
get value() { return this.counter; }
}
@Component({
selector: 'app-counter',
template: `
<button (click)="inc()">+1</button>
<p>Текущее значение: {{service.value}}</p>
`,
providers: [CounterService]
})
export class CounterComponent {
constructor(public service: CounterService) {}
inc() { this.service.inc(); }
}
Если вы вставите <app-counter></app-counter> дважды на страницу — у каждого будет свой независимый счетчик.
Когда это бывает полезно?
- Локальное состояние: Когда нужно, чтобы каждый компонент (или группа компонентов) имел свою копию сервиса (например, для независимых форм, таймеров, локальных кэшей).
- Изоляция: Для тестирования, чтобы не было "перетекания" состояния между разными частями приложения.
- Вложенные компоненты: Если вы передаёте сервис через providers в родительском компоненте, все дочерние компоненты могут получить тот же экземпляр (если не переопределят provider).
5. Полезные нюансы
Как Angular ищет сервис? Иерархия инжекторов
Когда Angular видит, что компоненту нужен сервис, он ищет его в следующем порядке:
- В массиве providers самого компонента (или ближайшего родителя в дереве компонентов).
- В массиве providers NgModule, которому принадлежит компонент.
- В корневом инжекторе (providedIn: 'root', AppModule).
Если сервис нигде не найден — будет ошибка "No provider for ...".
Переопределение сервисов: Shadowing
Иногда бывает нужно "заменить" сервис только для части приложения. Например, вы хотите, чтобы в тестовом компоненте использовался не настоящий сервис, а его заглушка (mock).
@Component({
selector: 'app-mock-user',
template: `<p>Mocked!</p>`,
providers: [{ provide: UserService, useClass: MockUserService }]
})
export class MockUserComponent {
constructor(private userService: UserService) {}
}
В этом случае только этот компонент (и его потомки) будут получать MockUserService, а остальные — обычный UserService.
6. Практика: примеры для вашего приложения
Давайте представим, что в нашем учебном приложении есть компонент "Корзина покупок" (CartComponent) и сервис CartService. Мы хотим, чтобы у каждой корзины на странице был свой сервис (например, если пользователь может работать с несколькими корзинами одновременно).
@Injectable()
export class CartService {
private items: string[] = [];
add(item: string) { this.items.push(item); }
getItems() { return this.items; }
}
@Component({
selector: 'app-cart',
template: `
<button (click)="addItem()">Добавить случайный товар</button>
<ul>
<li *ngFor="let item of service.getItems()">{{item}}</li>
</ul>
`,
providers: [CartService]
})
export class CartComponent {
constructor(public service: CartService) {}
addItem() {
const item = 'Товар #' + Math.floor(Math.random() * 1000);
this.service.add(item);
}
}
Вставьте <app-cart></app-cart> несколько раз — и убедитесь, что каждая корзина независима.
7. Типичные ошибки и особенности
Ошибка №1: Неожиданно много экземпляров сервиса
Если вы случайно укажете сервис в providers одновременно на уровне модуля и на уровне компонента, Angular создаст отдельный экземпляр для компонента, и он "затмит" модульный. Это может привести к неожиданному поведению, если вы рассчитывали на общий сервис.
Ошибка №2: Ожидание синглтона, но получаете разные экземпляры
Многие новички думают, что сервисы всегда синглтоны. Но если вы определяете providers на уровне компонента, синглтон превращается в "многотон" — у каждого компонента свой экземпляр. Всегда проверяйте, где объявлен provider.
Ошибка №3: Отсутствие провайдера
Если вы забыли объявить сервис в providers (или не используете providedIn), Angular выбросит ошибку DI: "No provider for ...". Не забывайте добавлять сервис либо в providedIn, либо в соответствующий массив.
Ошибка №4: Переопределение сервиса не там, где нужно
Если вы переопределяете сервис на уровне компонента, а ожидали переопределить его для всего модуля, убедитесь, что вы правильно выбрали уровень. Shadowing работает только вниз по иерархии.
Ошибка №5: "Лишние" зависимости
Иногда сервис зависит от других сервисов, которые предоставлены только на определённом уровне. Если вы используете сервис в компоненте, а его зависимости не объявлены — получите ошибку при инициализации.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ