Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров3.4K

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: '<app-child></app-child>',
  providers: [{ provide: MyService, useClass: ParentService }],
})
export class ParentComponent {}

@Component({
  selector: 'app-child',
  template: '<p>{{ service.getData() }}</p>',
})
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: '<p>Проверьте консоль</p>',
})
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<string, boolean>();

  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: `<p *ngIf="enabled">This feature is enabled!</p>`,
})
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: `
    <p *ngIf="isFeatureAEnabled">FeatureA Enabled!</p>
    <p *ngIf="isFeatureBEnabled">FeatureB Enabled!</p>
  `,
  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-контейнера, иерархии инжекторов и кастомных провайдеров даёт возможность разрабатывать сложные проекты с более глубоким контролем зависимостей.

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

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

Теги:
Хабы:
Всего голосов 6: ↑5 и ↓1+4
Комментарии0

Публикации

Ближайшие события