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

c96dcdd6907c4d3e620812fe75dca0ed.png

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

Введение в Dependency Injection

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

DI-контейнер обеспечивает:

  • Повторное использование сервисов: Один экземпляр сервиса может использоваться в разных местах.

  • Модульность: Каждый модуль может иметь свои зависимости, изолированные от других.

  • Тестируемость: Подменяя зависимости моками, вы можете писать юнит-тесты без имплементации цепочек зависимостей.

Как устроен DI-контейнер Angular?

DI-контейнер Angular базируется на трех концептах:

  1. Провайдеры: Они описывают, как создавать экземпляры объектов, которыми будут управлять DI.

  2. Инжекторы: Они хранят информацию о провайдерах и создают зависимости.

  3. Иерархия инжекторов: Каждый инжектор может иметь свои провайдеры. Если зависимость не найдена в текущем инжекторе, 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 создает три уровня инжекторов:

  1. Корневой инжектор (root): Управляет сервисами, зарегистрированными с помощью providedIn: 'root'.

  2. Инжектор компонентов: Создается для каждого компонента, который использует providers.

  3. Инжектор директив/модулей: Создается для модулей или директив с локальными провайдерами.

Пример:

@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-контейнера,  иерархии инжекторов и кастомных провайдеров даёт возможность разрабатывать сложные проекты с более глубоким контролем зависимостей.

В следующей статье мы рассмотрим,  как создавать динамические провайдеры и управлять жизненным циклом сервисов для специфичных сценариев.

Спасибо за внимание!  Оставляйте свои мысли и вопросы в комментариях.

© Habrahabr.ru