🅰️ День 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:

  1. Чистый синтаксис: Сервис внедряется одной строчкой прямо при объявлении свойства класса, как кастомный хук в React.
  2. Композиция логики: Вы можете использовать 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

InjectionToken в Angular 20+ — это...

Шаг 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 разработчика (В чём профит?):

  1. Разрыв жесткой связи (Decoupling): В React, если вы импортируете класс или кастомный хук напрямую в дочерний компонент, вы не можете подменить его рантаймом без изменения кода ребенка. В Angular токен выступает прослойкой.
  2. Dynamic Resolution: Конструкция useFactory позволяет конструировать объекты на ходу. Вы можете внедрить в саму фабрику другие токены (например, inject(HttpClient)) и вернуть кастомизированный продукт в зависимости от ответа сервера или состояния приложения.
  3. Бесшовный Мокинг в Тестах: В файле тегов spec.ts для проверки Day5ChildNode вам достаточно написать:
    {
     provide: LAB_LOGGER,
     useValue: {
       log: jasmine.createSpy('log'),
       prefix: 'mock'
     }
    }
    ...и компонент протестируется в полной изоляции от реальной консоли или серверов.

Исходники примера 👉 здесь