Привет, Хабр!

В этой статье мы рассмотрим, как работает Injector в Angular, зачем нужны декораторы @Optional, @SkipSelf, @Host, и чем отличаются провайдеры на уровне root, модуля и компонента.

Как Angular находит зависимости: а он просто идёт вверх

В Angular нет ничего волшебного. Injector — это просто иерархическая система, которая ищет твою зависимость снизу вверх по дереву инжекторов.

AppInjector (корень приложения)
  ├── ModuleInjector (фичевые модули)
  │     └── ComponentInjector (каждый компонент)
  │             └── DirectiveInjector (директивы/провайдеры внутри)

Angular смотрит: “Ага, тут у меня компонент. Он хочет MyService. Сначала проверю, есть ли у него локальный провайдер. Нет? Поднимусь выше — в модуль. Нет? Тогда в AppModule. Нашёл? Окей, вот.”

Простой пример:

@Injectable()
export class LocalLogger {
  log(msg: string) {
    console.log('[LOCAL]', msg);
  }
}

@Component({
  selector: 'logger-demo',
  template: `<p>Check console</p>`,
  providers: [LocalLogger] // локальный провайдер
})
export class LoggerComponent {
  constructor(logger: LocalLogger) {
    logger.log('Hello from LoggerComponent');
  }
}

В этом случае LocalLogger создаётся прямо внутри компонента, и даже если выше по иерархии есть другой Logger, этот будет использоваться. Это поведение по дефолту. А вот дальше становится интересно...

Почему @Optional, @SkipSelf, @Host — очень точные указки

@Optional()

Если Angular не найдёт нужную зависимость, он бросит исключение. Но иногда нужно: "если есть — хорошо, если нет — не беда". Вот тогда и нужен @Optional().

@Component({
  selector: 'optional-demo',
  template: `...`,
})
export class OptionalComponent {
  constructor(@Optional() private config?: OptionalConfigService) {
    if (config) {
      config.apply();
    } else {
      console.warn('No OptionalConfigService found. Using defaults.');
    }
  }
}

Здесь если OptionalConfigService нигде не зарегистрирован — просто будет undefined, и приложение не упадёт.

@SkipSelf()

А он говорит: “Ищи, но не у меня. Я — не в счёт. Начинай с родителя.”

Типовой кейс:

@Injectable()
export class CoreService {}

@Component({
  selector: 'child',
  template: `...`,
  providers: [CoreService] // этот экземпляр игнорируем
})
export class ChildComponent {
  constructor(@SkipSelf() core: CoreService) {
    // будет взят CoreService из родителя, не из этого компонента
  }
}

Это суперполезно, когда хочется проксировать тот же сервис из родителя в потомков, не перебивая его локальной версией.

@Host()

Это уже более тонкий случай. @Host() говорит: “Остановись на компоненте, где используется эта директива. Выше — не лезь.”

@Directive({
  selector: '[track-host]',
})
export class TrackHostDirective {
  constructor(@Host() private logger: LocalLogger) {
    // найдёт только если LocalLogger в хост-компоненте, где повешена директива
  }
}

Это способ ограничить область поиска строго одним хостом. Выручает при написании сложных директив или компонентов, которые не должны зависеть от глобального контекста.

Уровни провайдеров

providedIn: 'root'

@Injectable({ providedIn: 'root' })
export class ApiService {}

Это значит — "создай один экземпляр этого сервиса на всё приложение". Singleton. И Angular подсовывает его через корневой AppInjector.

Он будет создан только если реально используется.

В модуле

@NgModule({
  providers: [AnalyticsService],
})
export class AnalyticsModule {}

Теперь AnalyticsService создаётся при подключении этого модуля и будет один на весь модуль.

Но вот засада: если ты подключил этот модуль несколько раз — в каждом будет свой экземпляр сервиса.

В компоненте

@Component({
  providers: [LocalStorageService],
})
export class MyComponent {}

Каждый инстанс компонента — своя копия сервиса.

Создаем сервис, который работает ТОЛЬКО внутри компонента или модуля

Сервис, который живёт строго внутри компонента, — мощный паттерн. Можно создать форму, у которой есть FormContextService, но она не должна быть доступна извне.

@Injectable()
export class FormContextService {
  private state = new BehaviorSubject<any>({});
  get value$() {
    return this.state.asObservable();
  }

  setValue(val: any) {
    this.state.next(val);
  }
}

Теперь повесим его только на компонент:

@Component({
  selector: 'app-form',
  providers: [FormContextService],
  template: `
    <input (input)="update($event.target.value)">
    <child-component></child-component>
  `
})
export class FormComponent {
  constructor(private ctx: FormContextService) {}

  update(val: string) {
    this.ctx.setValue({ input: val });
  }
}

А вот child-component:

@Component({
  selector: 'child-component',
  template: `
    <p *ngIf="val$ | async as val">{{ val.input }}</p>
  `
})
export class ChildComponent {
  val$ = this.ctx.value$;

  constructor(private ctx: FormContextService) {}
}

Оба компонента получают один и тот же экземпляр FormContextService, но он ограничен уровнем FormComponent. За его пределами его никто не увидит.

Если же мы захотим обойти это и получить контекст в родителе — придётся тащить @SkipSelf или @Host.

Вывод

Angular строит иерархию инжекторов, ты можешь писать гибкий, локализованный код. DI — это не просто способ втыкать сервисы, это способ управлять областью действия, жизненным циклом и архитектурой приложения.

Краткая выжимка:

  • providedIn: 'root' = singleton на всё приложение;

  • провайдер в модуле = singleton на модуль;

  • провайдер в компоненте = инстанс на каждый компонент;

  • @Optional = не паникуй, если не нашёл;

  • @SkipSelf = пропусти себя, иди вверх;

  • @Host = не выше хоста — ни шагу.

Пишите аккуратно и пишите осознанно, а так же делитесь своим опытом в комментарях.


Если вы хотите не просто использовать Angular, а понимать, как он работает изнутри — начните с основ внедрения зависимостей. Как работает Injector, зачем нужны @Optional, @SkipSelf и @Host, как управлять областью действия сервисов — обо всём этом мы подробно рассказали в новой статье.

А если хотите перейти от теории к практике — приходите на открытые уроки по Angular и фронтенд‑разработке. Это отличный способ увидеть процесс обучения изнутри, задать вопросы преподавателям и понять, подходит ли вам программа:

  1. Первый шаг в Angular — создаем приложение с нуля — 10 июля в 20:00

  2. Реактивное программирование в Angular — 24 июля в 20:00

Пройдите вступительное тестирование — узнайте, хватает ли ваших текущих знаний для обучения на курсе "Angular Developer".

Комментарии (1)


  1. itsukenberg
    03.07.2025 10:42

    А providedIn: platform отменили?