1. Как Angular ищет сервис: базовая схема
В Angular DI — это не просто «магия», а довольно стройная система поиска зависимостей. Представьте себе матрёшку: внутри приложения есть модули, внутри модулей — компоненты, а внутри компонентов — ещё компоненты. На каждом уровне может быть свой «контейнер» сервисов, который называется инжектором.
Когда компоненту нужен сервис, Angular идёт по иерархии инжекторов — снизу вверх — и ищет, где этот сервис был впервые объявлен. Если не находит — выбрасывает ошибку (или, простите, Exception: NullInjectorError, что звучит уже почти как диагноз).
Зачем это знать?
- Чтобы понимать, почему иногда сервис оказывается «новым» для каждого компонента, а иногда — один на всё приложение.
- Чтобы уметь создавать сервисы с нужной областью видимости (например, отдельный сервис только для одного модуля или даже компонента).
- Чтобы не ловить баги, когда данные «вдруг» не синхронизируются между разными частями приложения.
Что такое инжектор?
Инжектор — это объект, который знает, как создавать и хранить сервисы. Каждый модуль и компонент в Angular может иметь свой собственный инжектор.
- Корневой инжектор (root injector) — создаётся при старте приложения, содержит сервисы с providedIn: 'root' или объявленные в providers корневого модуля (AppModule).
- Инжектор модуля — появляется, если вы добавили сервис в providers какого-то модуля.
- Инжектор компонента — появляется, если вы добавили сервис в providers компонента.
Как происходит поиск?
- Компонент запрашивает сервис через конструктор.
- Angular смотрит в инжектор этого компонента — есть ли там нужный сервис?
- Если нет — поднимается вверх к родительскому компоненту.
- Если и там нет — поднимается к модулю, в котором объявлен компонент.
- Если не найден — идёт к корневому инжектору.
- Если нигде не найден — бросает ошибку.
Вот такая семейная цепочка поиска!
2. Пример: один сервис — разная область видимости
Давайте рассмотрим код, который иллюстрирует разные области видимости сервисов.
Сервис-счётчик
Создадим простой сервис, который хранит число и умеет его увеличивать:
// counter.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class CounterService {
private count = 0;
increment() {
this.count++;
}
get value() {
return this.count;
}
}
Вопрос: Где мы объявим этот сервис — в providedIn или в providers компонента?
Сценарий 1: Глобальный сервис (singleton)
@Injectable({ providedIn: 'root' })
export class CounterService { ... }
- Один экземпляр на всё приложение.
- Все компоненты, которые его внедряют, делят общее состояние.
Сценарий 2: Сервис только для компонента
// app.component.ts
@Component({
selector: 'app-root',
template: `<app-child></app-child><app-child></app-child>`,
providers: [CounterService] // Новый экземпляр для app-root и его потомков
})
export class AppComponent { }
Пояснение:
- Каждый раз, когда Angular встречает providers: [CounterService], он создаёт новый экземпляр сервиса для этого компонента и всех его потомков.
- Если в каждом app-child тоже прописать providers: [CounterService], то каждый получит свой собственный экземпляр.
3. Иллюстрация: "матрёшка" инжекторов
Вот как выглядит иерархия инжекторов в виде схемы:
[Root Injector]
|
[AppModule Injector]
|
[AppComponent Injector]
|
[ChildComponent Injector]
Пояснение:
- Если сервис объявлен на верхнем уровне — он общий для всех.
- Если сервис объявлен на уровне компонента — только этот компонент и его потомки видят этот экземпляр.
Аналогия:
Представьте, что вы пришли в супермаркет за молоком. Вы сначала ищете в своём холодильнике (инжектор компонента). Нет? Смотрите на кухне (инжектор родителя). Всё равно нет? Идёте в магазин (корневой инжектор). Если и там нет — ну, тут уже только слёзы.
4. Пример: разные экземпляры одного сервиса
Давайте докажем, что сервисы действительно могут быть разными:
// app.component.ts
@Component({
selector: 'app-root',
template: `
<app-child></app-child>
<app-child></app-child>
`,
providers: [CounterService] // Новый экземпляр для app-root и его потомков
})
export class AppComponent { }
// child.component.ts
@Component({
selector: 'app-child',
template: `
<button (click)="inc()">+</button>
<span>{{ counter.value }}</span>
`
})
export class ChildComponent {
constructor(public counter: CounterService) {}
inc() {
this.counter.increment();
}
}
- Если нажать "+" в одном app-child, оба компонента увидят изменение — потому что они делят один экземпляр сервиса (объявлён в родителе).
- Если добавить providers: [CounterService] в каждом app-child, то у каждого будет свой отдельный счётчик!
5. Как Angular выбирает инжектор: пример поиска
Рассмотрим последовательность поиска на примере:
@Component({
selector: 'parent',
providers: [CounterService], // тут создаётся экземпляр
template: `<child></child>`
})
export class ParentComponent { }
@Component({
selector: 'child',
template: `...`
})
export class ChildComponent {
constructor(private counter: CounterService) {}
}
Пояснение:
- Angular ищет CounterService в инжекторе ChildComponent — не находит.
- Поднимается к инжектору ParentComponent — находит и использует его экземпляр.
- Если бы в ChildComponent был свой providers: [CounterService], он бы использовал свой экземпляр.
6. Полезные нюансы
Сервис только для определённого модуля
Иногда нужно, чтобы сервис был доступен только внутри определённого модуля (например, AdminModule). Для этого добавьте его в providers этого модуля:
@NgModule({
declarations: [AdminComponent],
providers: [AdminService], // Только для этого модуля
})
export class AdminModule { }
Пояснение:
- Компоненты внутри AdminModule получат свой экземпляр AdminService.
- Компоненты вне модуля — не увидят этот сервис (если не импортируют модуль).
Особенности Standalone Components (Angular 14+)
В Standalone Components (компоненты без NgModule) всё аналогично: можно объявлять providers прямо в декораторе компонента.
@Component({
standalone: true,
selector: 'my-widget',
template: `...`,
providers: [WidgetService]
})
export class MyWidgetComponent { }
Пояснение:
- Каждый раз, когда этот компонент используется, создаётся свой экземпляр WidgetService.
- Если вы хотите делиться сервисом между несколькими Standalone Components, объявите его выше по иерархии (например, в родительском компоненте или в providedIn: 'root').
Переопределение сервисов
Если один и тот же сервис объявлен в providedIn: 'root' и в providers компонента — компонент и его потомки получат новый экземпляр, а не глобальный.
Сервисы в lazy-loaded модулях
Сервисы, объявленные в providers лениво загружаемых модулей, будут созданы только при загрузке этого модуля. Это удобно для разделения областей данных (например, отдельный сервис корзины для раздела "Магазин").
Сервисы в дочерних компонентах
Если вы объявили сервис в дочернем компоненте, только этот компонент и его подкомпоненты получат новый экземпляр. Остальные используют сервис из ближайшего родителя по иерархии.
7. Типичные ошибки при работе с иерархией инжекторов
Ошибка №1: Неожиданно разные экземпляры сервиса.
Разместили сервис в providers компонента, а ожидали, что он будет синглтоном — в результате каждый компонент работает со своим экземпляром, и данные не синхронизируются.
Ошибка №2: Неожиданно общий сервис.
Ожидали, что сервис будет уникальным для каждого модуля или компонента, но забыли добавить его в providers, и все используют глобальный экземпляр.
Ошибка №3: Дублирование логики.
Внедрили сервис в нескольких местах с разными областями видимости, получили неожиданные баги и несогласованность состояния.
Ошибка №4: Использование сервисов из lazy-модуля вне этого модуля.
Сервис объявлен только в лениво загружаемом модуле, а вы пытаетесь использовать его в основном приложении — получите ошибку провайдера.
Ошибка №5: Переопределение сервисов без необходимости.
Добавили сервис в providers компонента «на всякий случай», не понимая, что это создаёт новый экземпляр и может привести к путанице.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ