🅰️ День 3
Синхронизация с глобальным Roadmap: Этот конспект полностью покрывает Блок 3 (Дни 5–6) теоретического плана перехода. Мы изучаем мощнейший движок DI на уровне TS-классов, отказываемся от иерархии UI-дерева при управлении состоянием и внедряем легковесный стор через функциональный
inject().
Шаг 1: Движок DI и функция inject() (3 часа)
В React Context API жестко привязан к UI-дереву. Чтобы передать данные вниз, нужно обернуть компоненты в <Context.Provider>. В Angular движок DI работает на уровне рантайма инжекторов (классов и функций), полностью отделенных от визуального дерева.
Специфика Angular 20+ — полный отказ от старого синтаксиса внедрения через конструкторы классов в пользу функциональной функции inject().
Создание атомарного стенда
yarn ng generate component components/day-3/step-1 --selector="exp-day-3-step-1" --standalone --inline-template --inline-style --skip-testsСоздаем локальный токен/сервис и внедряем его в родительский и глубоко вложенный дочерний компоненты, демонстрируя иерархию инжекторов (NodeInjector) без написания провайдеров в HTML:
Для Senior React разработчика проще всего понять
@Injectableи@Componentчерез призму метапрограммирования и компиляции.
В React вы пишете чистые JS-функции, а связывание (рендеринг, контексты) происходит во время выполнения (runtime). В Angular архитектура декларативна: компилятор должен еще до запуска кода знать, какую роль выполняет каждый класс. Для этого используются декораторы (метаданные с префиксом @).
Два режима работы
@Injectable:
- Глобальный (Singleton / Zustand-way):
@Injectable({ providedIn: 'root' }) export class GlobalStore {}Движок DI создает один инстанс на все приложение. Полный аналог глобального стейт-менеджера.
- Локальный (Scoped / Context-way):
@Injectable() // Без providedIn export class LocalService {}Инстанс родится только тогда, когда какой-то компонент укажет его в массиве
providers: [LocalService]. Он умрет вместе с уничтожением этого компонента с экрана.
Итоговая шпаргалка для Дня 3
- Если классу нужен HTML-шаблон и он рендерится в DOM — это
@Component. Он отвечает за отображение и реактивное связывание данных с текстовыми нодами. - Если классу нужна только бизнес-логика, стейт или работа с API — это
@Injectable. Он отвечает за данные и может быть «внедрен» в@Componentодной строчкой черезinject().
src/app/components/day-3/step-1/step-1.ts
import { Component, Injectable, inject, signal } from '@angular/core';
/**
* 1. ИЗОЛИРОВАННЫЙ СЕРВИС (Бизнес-логика / Шина данных)
* Декоратор @Injectable заявляет рантайму Angular, что этот класс можно внедрять.
*/
@Injectable(
// providedIn: 'root' НЕ пишем, если нам нужен локальный стейт,
// привязанный строго к ветке этого компонента, а не глобальный синглтон.
)
export class CustomLoggerService {
// Реактивный лог, к которому получат доступ все, кто внедрит сервис
logs = signal<string[]>(['[System]: Инициализация локального инжектора']);
addLog(message: string) {
this.logs.update(current => [...current, `[${new Date().toLocaleTimeString()}]: ${message}`]);
}
}
/**
* 2. ГЛУБОКО ВЛОЖЕННЫЙ ДОЧЕРНИЙ КОМПОНЕНТ (Day3Step1Child)
*/
@Component({
selector: 'exp-day-3-step-1-child',
standalone: true,
imports: [],
template: `
<div class="lab-monitor stack1">
<h4>👶 Глубокий ребенок (Day3Step1Child)</h4>
<p style="font-size: 12px; color: #bbb;">
Я нахожусь на глубине дерева, у меня нет пропсов или колбэков, но я пишу напрямую в логгер.
</p>
<button (click)="sendAction()" class="btn">Запустить импульс из ребенка</button>
</div>
`,
})
export class Day3Step1Child {
// Настоящий аналог useContext() в Angular 20+.
// Функция inject() ищет CustomLoggerService вверх по иерархии инжекторов.
private logger = inject(CustomLoggerService);
sendAction() {
this.logger.addLog('⚡ Импульс сгенерирован внутри Day3Step1Child');
}
}
/**
* 3. РОДИТЕЛЬСКИЙ КОМПОНЕНТ (КОНТЕЙНЕР ШАГА)
*/
@Component({
selector: 'exp-day-3-step-1',
standalone: true,
// Импортируем дочерний компонент в скоуп standalone шаблона
imports: [Day3Step1Child],
// РЕГИСТРАЦИЯ ПРОВАЙДЕРА: Создает инстанс сервиса на уровне этого компонента и его детей.
// Это точечный аналог <Context.Provider value={...}>, но без раздувания HTML-дерева!
providers: [CustomLoggerService],
template: `
<div class="lab-sandbox dark stack1">
<h3>🅰️ День 3: Шаг 1 — Движок DI и Функция inject()</h3>
<button (click)="addParentLog()" class="btn">Добавить лог из родителя</button>
<!-- Вывод логов, которые пишут и родитель, и ребенок в один инстанс класса -->
<div class="lab-monitor stack1">
<h4>📊 Локальный логгер инжектора (NodeInjector):</h4>
<ul style="list-style: none; padding: 0; margin: 0; font-size: 13px;">
@for (log of logger.logs(); track $index) {
<li>{{ log }}</li>
}
</ul>
</div>
<!-- Никаких пропсов и колбэков! Ребенок сам возьмет логгер из контекста DI -->
<exp-day-3-step-1-child />
</div>
`,
})
export class Day3Step1 {
// Внедряем тот же самый инстанс логгера, который унаследует и ребенок
protected logger = inject(CustomLoggerService);
addParentLog() {
this.logger.addLog('🏗️ Действие зафиксировано в родителе Step1');
}
}Шаг 2: Паттерн «Сервис как Стор» и Singleton-состояния (3 часа)
В Шаге 2 создается глобальный синглтон-стор, который функционирует независимо от UI-дерева и доступен в любой точке приложения, выступая легковесной альтернативой громоздким стейт-менеджерам. Внедрение такого стора происходит через inject() в одну строчку, обеспечивая чистое разделение UI-слоя и состояния.
Создание атомарного стенда
yarn ng generate component components/day-3/step-2 --selector="exp-day-3-step-2" --standalone --inline-template --inline-style --skip-testssrc/app/components/day-3/step-2/step-2.ts
import { Component, Injectable, inject, signal, computed } from '@angular/core';
/**
* 1. ГЛОБАЛЬНЫЙ СИНГЛТОН-СТОР (Service-as-a-Store паттерн)
* providedIn: 'root' гарантирует, что Angular создаст ровно ОДИН инстанс
* этого класса на все приложение и уничтожит его только при закрытии вкладки.
*/
@Injectable({
providedIn: 'root'
})
export class SandboxStoreService {
// Приватное состояние (Инкапсуляция данных)
private readonly _isEngineOnline = signal<boolean>(true);
private readonly _subRoutinesCount = signal<number>(5);
// Публичное состояние в режиме Read-Only
isEngineOnline = this._isEngineOnline.asReadonly();
subRoutinesCount = this._subRoutinesCount.asReadonly();
// Вычисляемое состояние на уровне Стора (Computed Node в ациклическом графе)
systemStatus = computed(() => {
console.log('🟢 [Global Store Computed]: Вычисляю системный статус...');
return this._isEngineOnline()
? `⚙️ Рантайм Активен. Процессов в пуле: ${this._subRoutinesCount()}`
: '🔴 Рантайм Остановлен. Внимание.';
});
// Действия (Actions) для изменения состояния
toggleEngine() {
this._isEngineOnline.update(state => !state);
}
addSubRoutine() {
if (this._isEngineOnline()) {
this._subRoutinesCount.update(count => count + 1);
} else {
console.warn('⚠️ Невозможно добавить процесс: ядро системы выключено!');
}
}
}
/**
* 2. КОМПОНЕНТ ПУЛЬТА УПРАВЛЕНИЯ (Day3Step2)
*/
@Component({
selector: 'exp-day-3-step-2',
standalone: true,
imports: [],
// providers: [...] НЕ пишем, иначе мы перезапишем глобальный инстанс локальным!
template: `
<div style="padding: 20px; border: 2px dashed #3498db; font-family: monospace; margin-top: 20px;">
<h3>🅰️ День 3: Шаг 2 — Паттерн "Сервис как Стор" (Singleton)</h3>
<div style="background: #2c3e50; color: #fff; padding: 15px; border-radius: 4px; margin-bottom: 15px;">
<h4>📊 Монитор Глобального Состояния:</h4>
<p>Текущий статус: <strong>{{ store.systemStatus() }}</strong></p>
<p>Ядро онлайн: <strong [style.color]="store.isEngineOnline() ? '#0f0' : '#ff3333'">{{ store.isEngineOnline() }}</strong></p>
</div>
<div>
<button (click)="store.toggleEngine()">1. Переключить Питание Ядра</button>
<button (click)="store.addSubRoutine()" style="margin-left: 10px;">2. Запустить Sub-Routine</button>
</div>
<p style="color: #bbb; font-size: 12px; margin-top: 15px;">
*🔬 Этот стор полностью изолирован от UI-разметки. Изменение питания ядра не триггерит тяжелые вычисления в других компонентах.
</p>
</div>
`,
})
export class Step2 {
// Внедряем глобальный стор одной строчкой через inject().
// Шаблон компонента имеет прямой доступ к readonly сигналам стора.
protected store = inject(SandboxStoreService);
}🔬 Что этот код демонстрирует React-разработчику?
- Истинный синглтон без бойлерплейта: В React для создания глобального хранилища вам нужен Redux, Zustand или связка контекста с редьюсером. Здесь это обычный TS-класс с декоратором
@Injectable({ providedIn: 'root' }). Angular сам лениво инициализирует его при первом вызовеinject(). - Инкапсуляция стейта (
asReadonly): Компоненты могут читать состояние (store.isEngineOnline()), но не могут мутировать его в обход стора (например, написатьstore.isEngineOnline.set(false)вызовет ошибку TS). Изменения стейта происходят строго через декларативные методы-экшены самого класса стора.
🧪 Лабораторный чек-лист для проверки в браузере
При подключении компонентов Day3Step1 и Day3Step2 в корневой src/app/app.ts обратите внимание на следующие аспекты:
- Изоляция в Шаге 1: Дочерний компонент обновляет общий лог родителя напрямую без использования пропсов или колбэков благодаря DI-связыванию на уровне TypeScript.
- Изоляция вычислений в Шаге 2: Изменение статуса питания (ONLINE/OFFLINE) не вызывает лишних пересчетов
systemStatus, так как граф реактивности отслеживает только релевантные изменения свойств. - Чистота разметки: Шаблоны полностью очищены от инфраструктурного мусора вроде провайдеров контекста.
