🅰️ День 5
Для React-разработчика с 7-летним стажем DI — это самая большая «слепая зона». В React внедрение зависимостей происходит декларативно через UI-дерево (Context API), что намертво связывает логику приложения с рендерингом компонентов.
В Angular движок DI полностью отделен от визуального дерева DOM. Это мощнейший рантайм-инструмент на уровне классов и функций TypeScript, который управляет созданием, кэшированием и подменой инстансов («синглтонов») в экспериментальном приложении.
Архитектура движка: Две параллельные вселенные инжекторов
В Angular 20+ существует две независимые иерархии инжекторов, которые и определяют область видимости (Scope) ваших сервисов и данных. Именно с этим поведением мы столкнулись на Дне 4:
1. EnvironmentInjector (Маршруты и Модули)
Это глобальная иерархия, которая создается на этапе старта приложения и загрузки чанков роутера. Сервисы здесь инициализируются как Singleton на весь сешн или раздел.
providedIn: 'root'— сервис регистрируется в самом корневомEnvironmentInjector. Он доступен везде и скачивается лениво (Tree-shakable).providers: [...]внутри конфигурации путей Routes — создает инжектор на уровне конкретной ветки маршрутов. Как мы выяснили на Шаге 4, он сохраняет стейт при переходах между детьми, пока пользователь не покинет этот раздел.
2. NodeInjector (UI-Дерево Компонентов)
Это локальная иерархия, которая строится строго по структуре HTML-шаблонов ваших компонентов. Сервисы здесь привязаны к жизненному циклу конкретного тега в DOM.
providers: [...]внутри декоратора@Component— создает уникальный инстанс сервиса для этого компонента и всех его дочерних тегов. Если компонент уничтожается (например, скрылся под@if), стейт полностью стирается.
Новейший DX: Прощай constructor(), привет функция inject()
В старых статьях по Angular вы увидите внедрение зависимостей через конструктор класса (constructor(private api: NotesApiService)). В Angular 20+ стандартом стала функциональная инжекция с помощью функции inject().
Она имеет колоссальные преимущества для React Senior:
- Чистый синтаксис: Сервис внедряется одной строчкой прямо при объявлении свойства класса, как кастомный хук в React.
- Композиция логики: Вы можете использовать
inject()внутри обычных функций (например, в гвардах, резолверах или утилитах), создавая функциональные аналоги кастомных хуков React без привязки к классам.
Чтобы закрепить эту тему, мы создадим компонент src/app/components/day-5/step-1/step-1.ts с помощью CLI:
yarn ng generate component components/day-5/step-1 --selector="exp-day-5-step-1" --standalone --inline-template --inline-style --skip-testsШаг 1: Объявление контракта и создание InjectionToken
Создаем токен, который настраивается с помощью встроенного фабричного метода. Мы сразу закладываем ленивую инициализацию по умолчанию.
// src/app/tokens/logger.token.ts
import { InjectionToken, inject } from '@angular/core';
// 1. Интерфейс контракта (в React мы бы просто надеялись на duck-typing)
export interface LabLogger {
log(message: string): void;
readonly prefix: string;
}
// 2. Создаем сам токен. Он выступает уникальным ключом для движка DI
export const LAB_LOGGER = new InjectionToken<LabLogger>('LAB_LOGGER', {
providedIn: 'root', // Доступен везде по умолчанию (Tree-shakable)
factory: () => {
// Фабрика по умолчанию: если никто не переопределил токен выше по дереву инжекторов,
// Angular автоматически создаст стандартный консольный логгер.
return {
prefix: '🟢 [Default Core]',
log: (msg) => console.log(`🟢 [Default Core]: ${msg}`)
};
}
});Шаг 2: Альтернативные реализации логгера (Наши фабричные продукты)
Создадим продвинутый логгер для исследовательских лабораторий, который умеет динамически брать контекст из другого сервиса.
// Альтернативный продукт для NodeInjector
export class AdvancedLabLogger implements LabLogger {
constructor(public readonly prefix: string) {}
log(message: string): void {
console.warn(`${this.prefix} (Рантайм-трассировка): ${message}`);
}
}Шаг 3: Полный код компонента Day5Step1 с демонстрацией Абстрактной Фабрики
Мы пишем родительский компонент, который переопределяет фабрику токена прямо в своей секции providers. Обратите внимание, как дочерний компонент вообще ничего не знает о том, какой логгер ему прилетит — он просто запрашивает ключ LAB_LOGGER.
// src/app/components/day-5/step-1/step-1.ts
import { Component, inject, ComponentRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LAB_LOGGER, LabLogger, AdvancedLabLogger } from '../../../tokens/logger.token';
/**
* 1. ДОЧЕРНИЙ ВИДЖЕТ (Экспериментальная нода)
*/
@Component({
selector: 'exp-day-5-child-node',
standalone: true,
template: `
<div class="lab-monitor stack1">
<b>🔬 Дочерняя Нода Инжекции</b>
<p>Используемый префикс логгера: <strong style="color: #e74c3c;">{{ logger.prefix }}</strong></p>
<button (click)="runExperiment()" class="btn">
Отправить отчет в логгер
</button>
</div>
`
})
export class Day5ChildNode {
/**
* Магия Angular 20+: Мы внедряем контракт интерфейса по токену-ключу!
* Дочерний компонент изолирован. Ему всё равно, стандартный это логгер или AdvancedLabLogger.
*/
protected readonly logger = inject(LAB_LOGGER);
public runExperiment(): void {
this.logger.log('Экспериментальный процесс запущен успешно.');
}
}
/**
* 2. РОДИТЕЛЬСКИЙ КОМПОНЕНТ (Day5Step1)
*/
@Component({
selector: 'exp-day-5-step-1',
standalone: true,
imports: [CommonModule, Day5ChildNode],
template: `
<div class="lab-sandbox dark stack1">
<h3>🅰️ День 5: Шаг 1 — InjectionToken как Абстрактная Фабрика</h3>
<div class="lab-monitor stack1">
<p>Родительский контейнер переопределяет фабрику логов для этой ветки DOM-дерева.</p>
<!-- Дочерний компонент получит кастомный логгер из NodeInjector родителя -->
<exp-day-5-child-node />
</div>
</div>
`,
// ИСПОЛЬЗУЕМ ДИНАМИЧЕСКИЙ USEFACTORY (Паттерн Абстрактная Фабрика)
providers: [
{
provide: LAB_LOGGER, // При запросе этого токена...
useFactory: (): LabLogger => {
// Здесь может быть любая сложная логика рантайма:
// проверка флагов окружения, чтение стейта, или генерация уникальных инстансов.
const isLabBeta = true;
if (isLabBeta) {
return new AdvancedLabLogger('⚠️ [Beta-Factory-Node #109]');
}
// Фолбек к дефолтной реализации
return {
prefix: '🟢 [Fallback Production]',
log: (msg) => console.log(`🟢 [Fallback]: ${msg}`)
};
}
}
]
})
export class Day5Step1 {}🔬 Чек-лист Senior React разработчика (В чём профит?):
- Разрыв жесткой связи (Decoupling): В React, если вы импортируете класс или кастомный хук напрямую в дочерний компонент, вы не можете подменить его рантаймом без изменения кода ребенка. В Angular токен выступает прослойкой.
- Dynamic Resolution: Конструкция
useFactoryпозволяет конструировать объекты на ходу. Вы можете внедрить в саму фабрику другие токены (например,inject(HttpClient)) и вернуть кастомизированный продукт в зависимости от ответа сервера или состояния приложения. - Бесшовный Мокинг в Тестах: В файле тегов
spec.tsдля проверкиDay5ChildNodeвам достаточно написать:...и компонент протестируется в полной изоляции от реальной консоли или серверов.{ provide: LAB_LOGGER, useValue: { log: jasmine.createSpy('log'), prefix: 'mock' } }
Исходники примера 👉 здесь
