Привет, Хабр!
В этой статье мы рассмотрим, как работает 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 и фронтенд‑разработке. Это отличный способ увидеть процесс обучения изнутри, задать вопросы преподавателям и понять, подходит ли вам программа:
Первый шаг в Angular — создаем приложение с нуля — 10 июля в 20:00
Реактивное программирование в Angular — 24 июля в 20:00
Пройдите вступительное тестирование — узнайте, хватает ли ваших текущих знаний для обучения на курсе "Angular Developer".
itsukenberg
А providedIn: platform отменили?