Как работает Dependency Injection в Angular?

Dependency Injection (DI) в Angular — это механизм, с помощью которого фреймворк автоматически предоставляет экземпляры зависимостей (обычно сервисов, но не только) в нужных местах приложения. DI — это фундамент Angular-приложений, обеспечивающий слабую связанность, повторное использование кода, инверсию управления и удобное тестирование.

1. Что такое зависимость?

Зависимость — это объект, необходимый другому объекту для выполнения своей работы. Например, компоненту может быть нужен AuthService для проверки авторизации. Этот сервис является его зависимостью.

2. Инъекция зависимостей (DI) — суть

Вместо того чтобы создавать зависимости вручную (new AuthService()), Angular сам создаёт экземпляр и внедряет его в нужный компонент, сервис или директиву. Это делается через механизм инъекции зависимостей, который реализует контейнер зависимостей — объект, хранящий все зарегистрированные зависимости и отвечающий за их создание и предоставление.

3. Как Angular узнаёт, что нужно внедрить?

Через конструктор класса:

@Component({ ... })
export class MyComponent {
constructor(private authService: AuthService) {}
}

Angular при создании компонента анализирует параметры конструктора и находит AuthService по типу. Он ищет зарегистрированный провайдер этого типа, создаёт (если ещё не создан) его экземпляр и передаёт в конструктор.

4. Роль @Injectable()

Чтобы Angular мог внедрять класс, он должен быть аннотирован декоратором @Injectable(). Этот декоратор говорит Angular, что класс может участвовать в системе DI и в нём могут быть зависимости:

@Injectable({ providedIn: 'root' })
export class AuthService {
constructor(private http: HttpClient) {}
}

Если providedIn указан, Angular автоматически регистрирует класс как провайдер.

5. Провайдеры (Providers)

Провайдер сообщает Angular, как создать объект. Они указываются:

  • В декораторе @Injectable({ providedIn })

  • В providers: [] у компонентов, модулей или директив

Пример:

@NgModule({
providers: \[AuthService\]
})
export class AppModule {}

Провайдер может быть определён несколькими способами:

Вид Пример
useClass { provide: AuthService, useClass: AuthService }
--- ---
useValue { provide: CONFIG, useValue: { api: 'url' } }
--- ---
useExisting { provide: AltLogger, useExisting: LoggerService }
--- ---
useFactory { provide: Service, useFactory: factoryFn, deps: [Dep1, Dep2] }
--- ---

6. Области внедрения (инжекторы)

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

Уровни:

  1. Root injector — глобальный. Всё, что зарегистрировано через providedIn: 'root'.

  2. Module injector — для ленивых модулей. Сервис создаётся заново при загрузке модуля.

  3. Component injector — если указано в providers компонента.

Поиск идёт от самого локального к глобальному инжектору. Если в компоненте зарегистрирован собственный провайдер, Angular создаст новый экземпляр зависимости:

@Component({
providers: \[MyService\] // новый экземпляр только для этого компонента
})

7. Singleton поведение

Сервисы, зарегистрированные в root, являются синглтонами — создаются один раз за всё время жизни приложения. Это удобно для хранения состояния (например, корзина товаров).

Но можно явно создать scoped-сервисы, зарегистрировав их:

  • В ленивом модуле — новый экземпляр на каждый лениво загруженный модуль;

  • В компоненте — новый экземпляр на каждый экземпляр компонента.

8. Внедрение интерфейсов — InjectionToken

TypeScript-интерфейсы не существуют в рантайме, поэтому их нельзя инжектировать напрямую. Для этого используют InjectionToken:

export interface Config {
apiUrl: string;
}
export const CONFIG_TOKEN = new InjectionToken<Config>('ConfigToken');
@NgModule({
providers: \[
{ provide: CONFIG_TOKEN, useValue: { apiUrl: 'https://api.example.com' } }
\]
})

Использование:

constructor(@Inject(CONFIG_TOKEN) private config: Config) {}

9. Использование @Optional(), @Self(), @SkipSelf(), @Host()

Angular предоставляет дополнительные токены-инструкции, уточняющие поведение DI:

Декоратор Назначение
@Optional() Не выбрасывает ошибку, если зависимость не найдена
--- ---
@Self() Ищет зависимость только в локальном инжекторе
--- ---
@SkipSelf() Пропускает локальный инжектор, начинает искать с родительского
--- ---
@Host() Ищет только до ближайшего @Host-компонента
--- ---

Пример:

constructor(@Optional() @Inject(SomeToken) private service?: SomeService) {}

10. Внедрение зависимостей через inject() (Angular 14+)

Начиная с Angular 14, появился способ получения зависимостей вне конструктора:

import { inject } from '@angular/core';
const auth = inject(AuthService); // можно использовать в функциях или фабриках

Это удобно в InjectionToken, SignalStore, фабриках, утилитах и т.д.

11. DI в директивах, пайпах и сервисах

Зависимости можно внедрять не только в компоненты, но и:

  • В директивы (через конструктор);

  • В пайпы (если они нестатичны);

  • В другие сервисы (service-in-service).

@Directive({ selector: '\[appExample\]' })
export class ExampleDirective {
constructor(private el: ElementRef, private logger: LoggerService) {}
}

12. DI при ленивой загрузке

При использовании lazy loading создаётся новый инжектор для модуля. Если сервис зарегистрирован внутри ленивого модуля, Angular создаст новый экземпляр только для этого модуля:

@NgModule({
providers: \[ScopedService\]
})
export class LazyModule {}

Это позволяет инкапсулировать зависимости и улучшить производительность.

13. DI при тестировании

При модульном тестировании можно подменить зависимости через TestBed:

TestBed.configureTestingModule({
providers: \[
{ provide: AuthService, useClass: MockAuthService }
\]
});

Или заменить конкретную зависимость:

TestBed.overrideProvider(AuthService, { useValue: mockAuth });

14. Динамическое создание зависимостей (ViewContainerRef, createComponent)

Если компонент создаётся динамически, его зависимости всё равно разрешаются через DI:

const componentRef = viewContainerRef.createComponent(MyComponent);

Angular автоматически инжектирует зависимости, используя текущий инжектор.

15. Внедрение зависимостей в standalone компоненты (Angular 14+)

В standalone компонентах и сервисах DI работает аналогично:

@Injectable()
export class MyService {}
@Component({
standalone: true,
providers: \[MyService\],
...
})
export class MyStandaloneComponent {
constructor(private myService: MyService) {}
}

Также можно использовать provide*() API:

bootstrapApplication(AppComponent, {
providers: \[provideHttpClient(), provideRouter(\[...\])\]
});

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