JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Иерархия инжекторов: как Angular ищет сервис

Иерархия инжекторов: как Angular ищет сервис

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

1. Как Angular ищет сервис: базовая схема

В Angular DI — это не просто «магия», а довольно стройная система поиска зависимостей. Представьте себе матрёшку: внутри приложения есть модули, внутри модулей — компоненты, а внутри компонентов — ещё компоненты. На каждом уровне может быть свой «контейнер» сервисов, который называется инжектором.

Когда компоненту нужен сервис, Angular идёт по иерархии инжекторов — снизу вверх — и ищет, где этот сервис был впервые объявлен. Если не находит — выбрасывает ошибку (или, простите, Exception: NullInjectorError, что звучит уже почти как диагноз).

Зачем это знать?

  • Чтобы понимать, почему иногда сервис оказывается «новым» для каждого компонента, а иногда — один на всё приложение.
  • Чтобы уметь создавать сервисы с нужной областью видимости (например, отдельный сервис только для одного модуля или даже компонента).
  • Чтобы не ловить баги, когда данные «вдруг» не синхронизируются между разными частями приложения.

Что такое инжектор?

Инжектор — это объект, который знает, как создавать и хранить сервисы. Каждый модуль и компонент в Angular может иметь свой собственный инжектор.

  • Корневой инжектор (root injector) — создаётся при старте приложения, содержит сервисы с providedIn: 'root' или объявленные в providers корневого модуля (AppModule).
  • Инжектор модуля — появляется, если вы добавили сервис в providers какого-то модуля.
  • Инжектор компонента — появляется, если вы добавили сервис в providers компонента.

Как происходит поиск?

  1. Компонент запрашивает сервис через конструктор.
  2. Angular смотрит в инжектор этого компонента — есть ли там нужный сервис?
  3. Если нет — поднимается вверх к родительскому компоненту.
  4. Если и там нет — поднимается к модулю, в котором объявлен компонент.
  5. Если не найден — идёт к корневому инжектору.
  6. Если нигде не найден — бросает ошибку.

Вот такая семейная цепочка поиска!

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();
  }
}
  1. Если нажать "+" в одном app-child, оба компонента увидят изменение — потому что они делят один экземпляр сервиса (объявлён в родителе).
  2. Если добавить 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 компонента «на всякий случай», не понимая, что это создаёт новый экземпляр и может привести к путанице.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ