Перейти к содержимому

13. Dependency Injection

DI (Dependency Injection) — это система управления зависимостями. Вместо того чтобы самому создавать объекты (new UserService()), ты говоришь Angular: «мне нужен UserService» — и фреймворк сам найдёт, создаст и передаст его. Это называется Голливудский принцип: «Не звони нам — мы позвоним тебе» 🎬


// ❌ Без DI: жёсткие зависимости, невозможно тестировать
class OrderComponent {
private userService = new UserService(
new HttpClient(/* ... */),
new LoggingService()
);
// Как подменить UserService в тестах? 😱
}
// ✅ С DI: Angular сам создаёт и передаёт зависимости
class OrderComponent {
constructor(private userService: UserService) {}
// В тестах легко передать MockUserService ✅
}

Angular создаёт дерево инжекторов, которое отражает структуру приложения. Когда компонент запрашивает зависимость, Angular ищет её снизу вверх по дереву:

Root Injector (AppModule / bootstrapApplication)
├── Platform Injector
│ └── ApplicationRef
├── Module Injector (FeatureModule)
│ └── Компоненты модуля
└── Component Injector (AppComponent)
├── Component Injector (HeaderComponent)
│ └── Component Injector (NavItemComponent)
└── Component Injector (DashboardComponent)
└── Component Injector (WidgetComponent)
// Запрос зависимости идёт снизу вверх:
// WidgetComponent → DashboardComponent → AppComponent → Module → Root
// Angular берёт ПЕРВЫЙ найденный провайдер
@Component({
selector: 'app-widget',
template: `...`
})
export class WidgetComponent {
// Angular ищет LoggingService начиная с этого компонента
// и идёт вверх по дереву инжекторов
constructor(private logger: LoggingService) {}
}

🎟️ InjectionToken — токены для не-классовых зависимостей

Заголовок раздела «🎟️ InjectionToken — токены для не-классовых зависимостей»

Токены нужны для провайдинга примитивов (строк, объектов, функций) — того, что нельзя идентифицировать по классу:

import { InjectionToken } from '@angular/core';
// Создаём типизированный токен
export const API_URL = new InjectionToken<string>('API_URL');
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
export const MAX_RETRIES = new InjectionToken<number>('MAX_RETRIES');
// Регистрируем провайдеры
@NgModule({
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' },
{ provide: MAX_RETRIES, useValue: 3 },
{
provide: APP_CONFIG,
useValue: {
version: '1.0.0',
features: { darkMode: true, analytics: false }
}
}
]
})
export class AppModule {}
// Используем в сервисе
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(
private http: HttpClient,
@Inject(API_URL) private apiUrl: string, // 👈 @Inject для токенов
@Inject(MAX_RETRIES) private maxRetries: number // 👈 Angular не знает тип по токену
) {}
get<T>(endpoint: string): Observable<T> {
return this.http.get<T>(this.apiUrl + endpoint).pipe(
retry(this.maxRetries)
);
}
}

// Интерфейс (контракт)
abstract class NotificationService {
abstract notify(message: string): void;
}
// Реальная реализация
@Injectable()
class ToastNotificationService implements NotificationService {
notify(message: string): void {
// Показываем toast через DOM
this.toastLib.show(message);
}
}
// Альтернативная реализация для тестов
@Injectable()
class MockNotificationService implements NotificationService {
messages: string[] = [];
notify(message: string): void {
this.messages.push(message); // Просто сохраняем
}
}
// В основном приложении
providers: [
{ provide: NotificationService, useClass: ToastNotificationService }
]
// В тестах — подменяем реализацию!
providers: [
{ provide: NotificationService, useClass: MockNotificationService }
]
// Конфигурация окружения
export const ENVIRONMENT_CONFIG = new InjectionToken<Environment>('ENVIRONMENT');
providers: [
{
provide: ENVIRONMENT_CONFIG,
useValue: {
production: false,
apiUrl: 'http://localhost:3000',
wsUrl: 'ws://localhost:3000',
version: '2.1.0'
}
}
]
// Использование
@Injectable({ providedIn: 'root' })
export class ConfigService {
constructor(@Inject(ENVIRONMENT_CONFIG) readonly config: Environment) {}
get apiUrl(): string { return this.config.apiUrl; }
}
export const ANALYTICS_SERVICE = new InjectionToken<AnalyticsService>('Analytics');
providers: [
{
provide: ANALYTICS_SERVICE,
useFactory: (config: AppConfig, logger: LoggingService) => {
// Решаем какую реализацию использовать в runtime
if (config.production) {
return new GoogleAnalyticsService(config.gaId, logger);
} else {
return new ConsoleAnalyticsService(logger);
}
},
deps: [APP_CONFIG, LoggingService] // 👈 Зависимости фабрики
}
]
// Создаём псевдоним — оба токена указывают на ОДИН экземпляр
providers: [
AuthService,
{
provide: UserContextService,
useExisting: AuthService // 👈 НЕ создаёт новый экземпляр!
}
]
// Компонент запрашивает UserContextService — получает AuthService
constructor(private userCtx: UserContextService) {}

export const HTTP_INTERCEPTORS = new InjectionToken<HttpInterceptor[]>('HTTP_INTERCEPTORS');
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true // 👈 Добавляем В МАССИВ, не заменяем
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoggingInterceptor,
multi: true // 👈 Теперь [AuthInterceptor, LoggingInterceptor]
},
{
provide: HTTP_INTERCEPTORS,
useClass: CacheInterceptor,
multi: true // 👈 [AuthInterceptor, LoggingInterceptor, CacheInterceptor]
}
]

@Component({
selector: 'app-form-field',
template: `...`
})
export class FormFieldComponent {
constructor(
// @Optional — не падаем если провайдер не найден, получаем null
@Optional() private tooltip: TooltipService | null,
// @Self — ищем ТОЛЬКО в инжекторе этого компонента
@Self() private formControl: NgControl,
// @SkipSelf — пропускаем инжектор этого компонента, ищем выше
@SkipSelf() private parentForm: FormGroupDirective,
// @Host — ищем только в этом компоненте и его host-компоненте
@Host() private hostRef: ElementRef
) {
if (!this.tooltip) {
console.log('TooltipService не зарегистрирован — это ок');
}
}
}

⚡ inject() — функциональная инъекция (Angular 14+)

Заголовок раздела «⚡ inject() — функциональная инъекция (Angular 14+)»

Функция inject() позволяет получить зависимости без конструктора. Идеальна для функций, guards, interceptors:

import { inject } from '@angular/core';
// В сервисе — вместо constructor
@Injectable({ providedIn: 'root' })
export class ProductService {
// Вместо constructor(private http: HttpClient, private logger: LoggingService)
private http = inject(HttpClient);
private logger = inject(LoggingService);
private apiUrl = inject(API_URL);
getAll(): Observable<Product[]> {
this.logger.log('Fetching products...');
return this.http.get<Product[]>(this.apiUrl + '/products');
}
}
// В Route Guard (функциональный стиль Angular 15+)
export const authGuard = (): boolean | UrlTree => {
const authService = inject(AuthService); // 👈 inject() вне класса!
const router = inject(Router);
if (authService.isAuthenticated) {
return true;
}
return router.createUrlTree(['/login']);
};
// В HTTP Interceptor (функциональный стиль Angular 15+)
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService); // 👈 inject() в функции!
const token = authService.getToken();
if (token) {
const authReq = req.clone({
headers: req.headers.set('Authorization', 'Bearer ' + token)
});
return next(authReq);
}
return next(req);
};

🚀 APP_INITIALIZER — инициализация перед стартом

Заголовок раздела «🚀 APP_INITIALIZER — инициализация перед стартом»
import { APP_INITIALIZER } from '@angular/core';
// Выполняется ДО того как Angular покажет первый компонент
function initializeApp(configService: ConfigService): () => Promise<void> {
return () => configService.loadConfig(); // Загружаем конфиг до старта
}
@NgModule({
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
deps: [ConfigService],
multi: true
}
]
})
export class AppModule {}
@Injectable({ providedIn: 'root' })
export class ConfigService {
private config: AppConfig | null = null;
async loadConfig(): Promise<void> {
// Angular ждёт завершения этого промиса перед стартом!
const response = await fetch('/assets/config.json');
this.config = await response.json();
}
get<T>(key: keyof AppConfig): T {
return this.config?.[key] as T;
}
}