Dependency Injection под микроскопом: углубленный разбор DI-контейнера Angular с примерами

Angular предоставляет мощный механизм Dependency Injection (DI), который делает приложения модульными и тестируемыми. В этой статье мы рассмотрим базовые механизмы работы DI-контейнера Angular, разберем иерархию инжекторов, ключевые роли @Injectable и @Optional, а также рассмотрим создание кастомных провайдеров и их применение в сложных проектах.
Введение в Dependency Injection
Dependency Injection (внедрение зависимостей) — это паттерн проектирования, который позволяет классу получать зависимости извне, а не создавать их самостоятельно. Angular реализует этот паттерн с использованием DI-контейнера, который управляет объектами и их графом зависимостей.
DI-контейнер обеспечивает:
Повторное использование сервисов: Один экземпляр сервиса может использоваться в разных местах.
Модульность: Каждый модуль может иметь свои зависимости, изолированные от других.
Тестируемость: Подменяя зависимости моками, вы можете писать юнит-тесты без имплементации цепочек зависимостей.
Как устроен DI-контейнер Angular?
DI-контейнер Angular базируется на трех концептах:
Провайдеры: Они описывают, как создавать экземпляры объектов, которыми будут управлять DI.
Инжекторы: Они хранят информацию о провайдерах и создают зависимости.
Иерархия инжекторов: Каждый инжектор может иметь свои провайдеры. Если зависимость не найдена в текущем инжекторе, Angular поднимается по цепочке инжекторов.
Основы: @Injectable
Самый важный декоратор в DI — это @Injectable. Он указывает Angular, что данный класс можно трактовать как зависимость.
Пример простого сервиса:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root', // Сервис доступен во всем приложении
})
export class MyService {
getData() {
return 'Hello from MyService!';
}
}
Здесь используется providedIn: 'root', что означает Tree-shakable провайдер, который регистрируется на уровне корневого инжектора.
Иерархия инжекторов в Angular
Angular создает три уровня инжекторов:
Корневой инжектор (root): Управляет сервисами, зарегистрированными с помощью providedIn: 'root'.
Инжектор компонентов: Создается для каждого компонента, который использует providers.
Инжектор директив/модулей: Создается для модулей или директив с локальными провайдерами.
Пример:
@Component({
selector: 'app-parent',
template: ' ',
providers: [{ provide: MyService, useClass: ParentService }],
})
export class ParentComponent {}
@Component({
selector: 'app-child',
template: '{{ service.getData() }}
',
})
export class ChildComponent {
constructor(public service: MyService) {}
}
Здесь ParentComponent регистрирует свою версию MyService. Поэтому ChildComponent, находясь внутри ParentComponent, получит зависимость из локального инжектора. Если бы провайдер в ParentComponent отсутствовал, Angular поднялся бы выше, чтобы найти MyService в корневом инжекторе.
Использование @Optional
Иногда нам нужно, чтобы зависимость была опциональной, то есть могла отсутствовать в инжекторе. Для этого используется @Optional.
Пример:
import { Injectable, Component, Optional } from '@angular/core';
@Injectable()
export class LoggerService {
log(message: string) {
console.log(message);
}
}
@Component({
selector: 'app-example',
template: 'Проверьте консоль
',
})
export class ExampleComponent {
constructor(@Optional() private logger?: LoggerService) {
if (logger) {
logger.log('Logger подключен');
} else {
console.warn('LoggerService не найден');
}
}
}
Если LoggerService не будет зарегистрирован в DI-контейнере, Angular не выбросит ошибку, а просто вернет undefined.
Кастомные провайдеры
Angular поддерживает различные стратегии создания провайдеров. Попробуем написать несколько примеров.
Использование useValue
Вы можете регистрировать фиксированные значения:
const API_URL = 'https://api.example.com';
@NgModule({
providers: [{ provide: 'API_URL', useValue: API_URL }],
})
export class AppModule {}
Подключаем строку:
constructor(@Inject('API_URL') private apiUrl: string) {
console.log(apiUrl); // https://api.example.com
}
Использование useFactory
С помощью useFactory мы можем динамически генерировать значения.
import { FactoryProvider } from '@angular/core';
export function loggerFactory() {
return Math.random() > 0.5 ? new LoggerService() : null;
}
@NgModule({
providers: [
{ provide: LoggerService, useFactory: loggerFactory }, // Динамическое создание сервиса
],
})
export class AppModule {}
Пример из реальной жизни: Service Token
Когда у вас есть несколько реализаций одного сервиса, бывает полезно использовать токены.
Создаем токен:
import { InjectionToken } from '@angular/core';
export const AUTH_STRATEGY = new InjectionToken('AUTH_STRATEGY');
Регистрация токена:
@NgModule({
providers: [
{ provide: AUTH_STRATEGY, useClass: OAuthStrategy },
],
})
export class AppModule {}
Теперь мы можем использовать конкретную стратегию:
constructor(@Inject(AUTH_STRATEGY) private auth: AuthService) {
this.auth.authenticate();
}
Практический пример: DI для сложных проектов
В больших приложениях часто возникает необходимость добавлять функционал опционально, в зависимости от конфигурации или текущего окружения. Например, определенные фичи могут быть доступны только для прод-среды, конкретных пользователей или включаться через глобальный «toggle» (feature flag). Angular предлагает гибкие инструменты для обработки таких сценариев.
Feature Toggle: Общий подход
Суть механизма feature toggle заключается в том, чтобы управлять включением/выключением фич через единую логическую точку — сервис проверки возможностей.
Опциональные фичи
Ключевой задачей является создание сервиса FeatureToggleService, который:
Читает список фич из конфигурации или API.
Обеспечивает методы динамической проверки доступности.
Позволяет использовать DI-контейнер для переопределения поведения в зависимости от среды.
Пример реализации:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class FeatureToggleService {
private featureMap = new Map();
constructor() {
// Имитируем загрузку данных (например, через API или локальный конфиг)
this.featureMap.set('featureA', true); // Включена
this.featureMap.set('featureB', false); // Выключена
}
isFeatureEnabled(featureName: string): boolean {
return this.featureMap.get(featureName) || false;
}
}
С помощью этого сервиса мы можем контролировать активацию или деактивацию фич. Теперь инструмент внедрения доступен в любом компоненте, но это только начало.
Интеграция с DI: разные окружения
Главная сила Angular DI заключается в том, что вы можете динамически подменять реализацию сервиса в зависимости от изменения контекста приложения (например, Dev/Prod окружение).
Например, добавим альтернативный FeatureToggleService для Dev-окружения, который всегда возвращает true:
import { Injectable } from '@angular/core';
@Injectable()
export class DevFeatureToggleService {
isFeatureEnabled(featureName: string): boolean {
return true; // Все фичи всегда включены
}
}
Зарегистрируем сервис в AppModule, установив зависимость от окружения:
import { NgModule } from '@angular/core';
import { environment } from '../environments/environment';
import { FeatureToggleService } from './services/feature-toggle.service';
import { DevFeatureToggleService } from './services/dev-feature-toggle.service';
@NgModule({
providers: [
{
provide: FeatureToggleService,
useClass: environment.production ? FeatureToggleService : DevFeatureToggleService,
},
],
})
export class AppModule {}
Теперь при запуске приложения Angular автоматически выберет нужную реализацию в зависимости от текущего окружения.
Простая работа с компонентами
В компонентах Angular можно напрямую использовать FeatureToggleService для контроля отображаемых элементов:
import { Component } from '@angular/core';
import { FeatureToggleService } from './services/feature-toggle.service';
@Component({
selector: 'app-feature',
template: `This feature is enabled!
`,
})
export class FeatureComponent {
enabled: boolean;
constructor(private featureToggleService: FeatureToggleService) {
this.enabled = this.featureToggleService.isFeatureEnabled('featureA');
}
}
Если фича выключена, блок просто не будет рендериться.
Механизмы инжекторов для более сложных сценариев
В более сложных проектах мы можем воспользоваться механизмами Angular для работы с DI для управления кастомными фичами.
Работа с InjectionToken
Если у нас разные фичи содержат разную логику или конфигурации, мы можем использовать InjectionToken.
Создаём токен и регистрируем провайдер:
import { InjectionToken } from '@angular/core';
export const FEATURE_CONFIG = new InjectionToken<{ [key: string]: boolean }>('FeatureConfig');
@NgModule({
providers: [
{
provide: FEATURE_CONFIG,
useValue: { featureA: true, featureB: false },
},
],
})
export class AppModule {}
Теперь создадим сервис, использующий этот токен:
import { Inject, Injectable } from '@angular/core';
import { FEATURE_CONFIG } from './feature-token';
@Injectable({
providedIn: 'root',
})
export class FeatureConfigService {
constructor(@Inject(FEATURE_CONFIG) private config: { [key: string]: boolean }) {}
isFeatureEnabled(featureName: string): boolean {
return this.config[featureName] || false;
}
}
Использование в компоненте
Теперь, подключив новый сервис в компоненте, мы можем динамически изменять поведение приложения, основываясь на заранее заданной конфигурации:
@Component({
selector: 'app-detailed-feature',
standalone: true,
template: `
FeatureA Enabled!
FeatureB Enabled!
`,
imports: [NgIf]
})
export class DetailedFeatureComponent {
isFeatureAEnabled = false;
isFeatureBEnabled = false;
constructor(private featureService: FeatureConfigService) {
this.isFeatureAEnabled = this.featureService.isFeatureEnabled('featureA');
this.isFeatureBEnabled = this.featureService.isFeatureEnabled('featureB');
}
}
*Стоит, конечно, оговориться, что для больших проектов лучше избегать хранения конфигураций фич локально и вместо этого загружать их через API, но это уже совсем другая история.
Заключение
Dependency Injection — это сердце архитектуры Angular. Его гибкость и расширяемость позволяют создавать модульные и масштабируемые приложения. Понимание принципов работы DI-контейнера, иерархии инжекторов и кастомных провайдеров даёт возможность разрабатывать сложные проекты с более глубоким контролем зависимостей.
В следующей статье мы рассмотрим, как создавать динамические провайдеры и управлять жизненным циклом сервисов для специфичных сценариев.
Спасибо за внимание! Оставляйте свои мысли и вопросы в комментариях.