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: '<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-контейнера, иерархии инжекторов и кастомных провайдеров даёт возможность разрабатывать сложные проекты с более глубоким контролем зависимостей.
В следующей статье мы рассмотрим, как создавать динамические провайдеры и управлять жизненным циклом сервисов для специфичных сценариев.
Спасибо за внимание! Оставляйте свои мысли и вопросы в комментариях.
