Как работает 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 использует иерархическую систему инжекторов. Это значит, что зависимости ищутся по дереву компонентов и модулей:
Уровни:
-
Root injector — глобальный. Всё, что зарегистрировано через providedIn: 'root'.
-
Module injector — для ленивых модулей. Сервис создаётся заново при загрузке модуля.
-
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 — это мощная и гибкая система, которая позволяет инкапсулировать логику, избегать дублирования кода, организовывать масштабируемую архитектуру и делать код легко тестируемым и поддерживаемым.