13. Dependency Injection
🏗️ Dependency Injection в Angular
Заголовок раздела «🏗️ Dependency Injection в Angular»DI (Dependency Injection) — это система управления зависимостями. Вместо того чтобы самому создавать объекты (new UserService()), ты говоришь Angular: «мне нужен UserService» — и фреймворк сам найдёт, создаст и передаст его. Это называется Голливудский принцип: «Не звони нам — мы позвоним тебе» 🎬
🤔 Зачем DI?
Заголовок раздела «🤔 Зачем DI?»// ❌ Без 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) ); }}📦 Типы провайдеров
Заголовок раздела «📦 Типы провайдеров»useClass — подмена реализации
Заголовок раздела «useClass — подмена реализации»// Интерфейс (контракт)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 }]useValue — константные значения
Заголовок раздела «useValue — константные значения»// Конфигурация окружения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; }}useFactory — создание на основе runtime-данных
Заголовок раздела «useFactory — создание на основе runtime-данных»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] // 👈 Зависимости фабрики }]useExisting — алиас существующего провайдера
Заголовок раздела «useExisting — алиас существующего провайдера»// Создаём псевдоним — оба токена указывают на ОДИН экземплярproviders: [ AuthService, { provide: UserContextService, useExisting: AuthService // 👈 НЕ создаёт новый экземпляр! }]
// Компонент запрашивает UserContextService — получает AuthServiceconstructor(private userCtx: UserContextService) {}🔢 Multi Providers — массив значений
Заголовок раздела «🔢 Multi Providers — массив значений»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] }]🎛️ Декораторы: @Optional, @Self, @SkipSelf, @Host
Заголовок раздела «🎛️ Декораторы: @Optional, @Self, @SkipSelf, @Host»@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; }}