Angular
Как устроено Angular-приложение, как фреймворк узнаёт об изменениях, как работать с RxJS и что поменялось с Angular 13 до актуальной версии 22. На собеседовании это проверяют и как знание основ, и как готовность поддерживать или обновлять старое приложение.
Перед этой темой: JavaScript и TypeScript.
Из чего состоит Angular-приложение?
Заголовок раздела «Из чего состоит Angular-приложение?»Что это
Angular-приложение — дерево компонентов. Корневой компонент запускается в main.ts, остальные вложены в его шаблон.
- Компонент — класс с декоратором
@Componentи HTML-шаблон. Класс хранит данные и обработчики, шаблон их показывает. - Сервис — класс с общей логикой: вызовы API, кеш, текущий пользователь.
- Маршрутизатор — показывает компонент, который соответствует URL.
Как пользоваться
ng generate component ordersсоздаёт компонент.- Дочерние компоненты и директивы, которые нужны шаблону, перечислите в
importsдекоратора. - Сервис получите полем класса:
private orders = inject(OrderService).
Частая ошибка
Забыть добавить дочерний компонент в imports. Сборка упадёт с ошибкой 'app-x' is not a known element.
Как устроено
| Синтаксис в шаблоне | Что делает |
|---|---|
{{ total }} |
интерполяция: значение из класса в текст |
[disabled]="busy" |
привязка свойства: класс → DOM |
(click)="save()" |
привязка события: DOM → класс |
[(value)]="name" |
двусторонняя: свойство плюс событие valueChange |
- Директива меняет поведение элемента:
[class.active],ngModel. Компонент — это директива со своим шаблоном. - Структурные директивы
*ngIfи*ngForс Angular 17 заменены встроенными блоками@ifи@for. - NgModule против standalone. В Angular 13 компонент объявляли в
declarationsкакого-нибудь NgModule. С v19 компоненты standalone по умолчанию и перечисляют зависимости в своихimports.
Когда что выбирать
| Способ получить сервис | Когда |
|---|---|
inject(Service) в поле |
по умолчанию, так советует style guide Angular |
| параметр конструктора | старый код; работает как раньше |
Ловушки
inject()работает только в injection context: инициализаторы полей, конструктор, фабрики провайдеров, гарды. Вызов в обработчике события даст ошибку NG0203.- Сервис в
providersкомпонента — отдельный экземпляр на каждый экземпляр компонента, а не общий.
Правило
Новые компоненты — standalone, сервисы — providedIn: 'root', зависимости — через inject().
Под капотом
- AOT-компилятор превращает шаблон в функции создания и обновления DOM. Ошибки типов в привязках видны на сборке.
- Поиск провайдера идёт снизу вверх: сначала инжекторы элементов от компонента к корню дерева, затем environment-инжекторы — маршрута, корневой, платформенный. Не нашёл —
NullInjectorError.
Сравнение с ASP.NET Core
| Angular | Ближайший аналог в ASP.NET Core |
|---|---|
providedIn: 'root', @Service() (v22) |
AddSingleton |
providers маршрута |
экземпляр на раздел приложения; прямого аналога нет |
providers компонента |
экземпляр на компонент и его поддерево; scopes в .NET не иерархичны |
inject(X, { optional: true }) |
GetService вместо GetRequiredService |
Компромиссы
- Standalone делает зависимости компонента явными и позволяет лениво грузить отдельный компонент. NgModule поддерживается, а для перевода есть миграция
ng generate @angular/core:standalone. @Service()из v22 — короткая запись@Injectable({ providedIn: 'root' }). Он работает только сinject(), без параметров конструктора.inject()упрощает наследование: базовому классу не нуженsuper(...)с длинным списком зависимостей.- Иерархия позволяет дать странице своё состояние через
providers, но тот же механизм незаметно плодит дубли синглтонов.
Как проверить
Angular DevTools в dev-сборке показывает дерево компонентов и инжекторов и откуда пришёл каждый сервис. Дубли синглтонов видны там же.
Итог: DI в Angular иерархический: где объявлен провайдер, там и граница общего экземпляра.
import { Component, Injectable, inject } from '@angular/core';
@Injectable({ providedIn: 'root' }) // один экземпляр на приложение, как AddSingletonexport class GreetingService { greet(name: string) { return `Hello, ${name}!`; }}
@Component({ selector: 'app-greeting', // standalone по умолчанию с v19 templateUrl: './greeting.html',})export class Greeting { private readonly greetings = inject(GreetingService); // вместо параметра конструктора protected message = '';
protected say(name: string) { this.message = this.greetings.greet(name); }}<input #name value="Angular" /><button (click)="say(name.value)">Greet</button><p [class.empty]="!message">{{ message }}</p> <!-- после клика: Hello, Angular! -->- Чем
providedIn: 'root'отличается отprovidersв компоненте? — Первый даёт один экземпляр на приложение, второй — свой экземпляр на каждый экземпляр компонента. - Где можно вызывать
inject()? — В injection context: инициализаторы полей, конструктор, фабрики, гарды и интерцепторы; иначе NG0203. - Нужен ли NgModule в новом приложении? — Нет: с v19 компоненты standalone по умолчанию, NgModule остаётся для старого кода.
- Чем компонент отличается от директивы? — Компонент — директива со своим шаблоном; директива меняет поведение существующего элемента.
- Что значит tree-shakable провайдер? — При
providedIn: 'root'сервис, который нигде не внедрён, не попадёт в бандл.
Как работает обнаружение изменений: zone.js, OnPush, signals и zoneless?
Заголовок раздела «Как работает обнаружение изменений: zone.js, OnPush, signals и zoneless?»Что это
Change detection — проверка, совпадает ли то, что на экране, с данными компонентов. Если значение в привязке изменилось, Angular обновляет этот кусок DOM.
Что изменилось с Angular 13
- Тогда проверку запускал zone.js после каждого клика, таймера или HTTP-ответа — по всему дереву.
- Сейчас состояние держат в сигналах:
signal(0)создаёт значение,count()читает его,count.set(1)меняет. Angular знает, какие шаблоны читают сигнал, и обновляет только их. - С v21 новые приложения работают без zone.js, с v22 компоненты по умолчанию OnPush.
Частая ошибка
Менять массив внутри сигнала: items().push(x). Ссылка та же, сигнал не сообщит об изменении. Нужно items.update(list => [...list, x]).
Как устроено с zone.js
- zone.js подменяет асинхронные API браузера:
setTimeout,Promise,addEventListener, XHR. - Когда задача и её микрозадачи завершились,
NgZoneсообщает об этом, и Angular вызываетApplicationRef.tick(). - Дерево компонентов проверяется сверху вниз: каждое выражение шаблона сравнивается с прошлым значением.
- В dev-режиме второй проход ловит значения, изменившиеся во время проверки, —
ExpressionChangedAfterItHasBeenCheckedError.
Стратегии
| Стратегия | Когда компонент проверяется |
|---|---|
Eager (раньше Default) |
в каждом цикле |
OnPush (по умолчанию с v22) |
новый input по ссылке, событие в поддереве, markForCheck(), изменение прочитанного сигнала |
Сигналы
signal хранит значение, computed вычисляет производное — лениво и с кешем, effect выполняет побочное действие: лог, запись в localStorage, вызов сторонней библиотеки.
Ловушки
- Мутация объекта во входе OnPush-компонента: ссылка та же, проверки не будет.
- Поле, изменённое в
setTimeoutили в подписке, в OnPush-компоненте не отрисуется без сигнала илиmarkForCheck(). effect, который копирует один сигнал в другой. Для производных значений естьcomputedиlinkedSignal.
Правило
Состояние — в сигналах, входы — неизменяемые, производное — через computed.
Под капотом
- Сигналы образуют граф: шаблоны,
computedиeffect— потребители, сигналы — источники. Чтение сигнала в реактивном контексте регистрирует зависимость. set()помечает зависимые представления и планирует проверку. OnPush-предки служат только путём к ним и сами не перепроверяются.- Без zone.js проверку запускают только уведомления:
markForCheck()(его вызываетasyncpipe),ComponentRef.setInput, изменение сигнала из шаблона, обработчик события в шаблоне.
Цена zone.js
- Проверка после любого асинхронного события, даже если данные не менялись.
- Вес бандла, время старта и длинные стек-трейсы.
- Нативный
async/awaitzone.js перехватить не может, поэтому сборка переписывает его в генераторы.
Компромиссы при миграции
ng updateдо v21 добавляетprovideZoneChangeDetection(), до v22 —ChangeDetectionStrategy.Eagerкомпонентам без явной стратегии. Поведение сохраняется.- Компоненты переводят на OnPush-совместимый код: сигналы, неизменяемые входы,
asyncpipe. NgZone.onStableиonMicrotaskEmptyв zoneless не срабатывают — их заменяютafterNextRender.- Последним убирают
provideZoneChangeDetection()иzone.jsизpolyfills.
Как проверить
- В dev-сборке
provideCheckNoChangesConfig({ exhaustive: true, interval: 1000 })находит привязки, изменённые без уведомления. - Профайлер Angular DevTools показывает длительность каждого цикла и проверенные компоненты.
Итог: zoneless — переход от «проверять всё после любого события» к «обновлять то, о чём сообщили», и готовность к нему определяет код компонентов, а не флаг.
import { ChangeDetectionStrategy, Component, computed, effect, signal } from '@angular/core';
@Component({ selector: 'app-cart', templateUrl: './cart.html', // С v22 OnPush — по умолчанию; в Angular 13 его указывали явно changeDetection: ChangeDetectionStrategy.OnPush,})export class Cart { protected readonly items = signal<{ name: string; price: number }[]>([]); // Пересчитается только при изменении items protected readonly total = computed(() => this.items().reduce((sum, i) => sum + i.price, 0));
constructor() { effect(() => console.log('Items:', this.items().length)); // побочное действие, не состояние }
protected add() { // Новая ссылка: сигнал сообщит об изменении. // this.items().push(...) не сработает — ссылка та же this.items.update(list => [...list, { name: 'Book', price: 10 }]); }}<button (click)="add()">Add</button><p>Items: {{ items().length }}, total: {{ total() }}</p> <!-- после клика: Items: 1, total: 10 -->import { bootstrapApplication } from '@angular/platform-browser';import { provideZoneChangeDetection, provideZonelessChangeDetection } from '@angular/core';import { App } from './app/app';
// Angular 20: zoneless включают явно; с v21 это поведение по умолчаниюbootstrapApplication(App, { providers: [provideZonelessChangeDetection()] });
// Старое приложение после ng update до v21+ остаётся на zone.js так:// bootstrapApplication(App, { providers: [provideZoneChangeDetection()] });- Почему OnPush-компонент не обновился после
this.user.name = 'X'? — Ссылка на объект не изменилась; нужен новый объект или сигнал. - Чем
computedотличается отeffect? —computedвозвращает значение и кешируется,effectтолько выполняет побочное действие. - Что означает
ExpressionChangedAfterItHasBeenCheckedError? — Dev-проверка нашла привязку, которая изменилась во время цикла проверки. - Что нужно, чтобы приложение работало без zone.js? — Уведомлять Angular через сигналы,
markForCheckилиasyncpipe и не полагаться наNgZone.onStable. - Нужен ли
NgZone.runOutsideAngularв zoneless? — Нет, но вызов безвреден; в zone-приложении он убирает лишние проверки на частых событиях.
Как работать с RxJS в Angular?
Заголовок раздела «Как работать с RxJS в Angular?»Что это
Observable — источник, который после подписки выдаёт значения: ноль, одно или много, а затем завершается или падает с ошибкой.
| Признак | Promise | Observable |
|---|---|---|
| Когда начинается работа | сразу при создании | при подписке |
| Сколько значений | одно | сколько угодно |
| Отмена | нет | отписка (unsubscribe) |
Как пользоваться
http.get<User[]>('/api/users')возвращает Observable, запрос уйдёт при подписке.- В шаблоне:
users$ | async— подписка и отписка автоматически. - В коде:
.subscribe(users => ...).
Частая ошибка
Подписаться на поток, который сам не завершается (interval, router.events, Subject в сервисе), и не отписаться. Компонент уничтожен, а подписка живёт и держит его в памяти.
Как устроено
HttpClientвыдаёт одно значение и завершается. Каждая подписка — новый запрос, отписка до ответа отменяет его.- Операторы в
pipe()строят новый поток:map,filter,debounceTime,catchError.
Когда что выбирать
| Оператор | Новое значение, пока прошлое не завершено | Пример |
|---|---|---|
switchMap |
отменяет прошлое | поиск по мере ввода |
mergeMap |
выполняет параллельно | загрузка независимых файлов |
concatMap |
ставит в очередь | сохранения, где важен порядок |
exhaustMap |
игнорирует новое | кнопка «Отправить», вход |
Как отписываться
asyncpipe — в шаблоне.toSignal(obs$)превращает поток в сигнал и отписывается при уничтожении компонента;toObservable(sig)делает обратное.takeUntilDestroyed()(стабилен с v19) — для ручногоsubscribe. Вне injection context ему передаютDestroyRef.
Ловушки
- Два
asyncpipe на один HTTP-поток — два запроса. Общий результат — черезshareReplayилиtoSignal. takeUntil(destroy$)не последним вpipe(): операторы после него могут оставить подписку живой.- Вложенный
subscribeвнутриsubscribeвместоswitchMap: подписки не отменяются, ошибки теряются.
Правило
Подписку создаёт шаблон или toSignal; ручной subscribe — только с takeUntilDestroyed().
Под капотом
- Observable — функция подписки.
pipe()ничего не запускает, аsubscribe()строит цепочку подписчиков до источника. - Отписка идёт по цепочке вверх. Для
HttpClientэто отмена запроса:AbortControllerу fetch-бэкенда (по умолчанию с v22) илиabort()у XHR. toObservableотдаёт значения черезeffect, поэтому несколькоset()подряд дадут одно значение — последнее.
Цена
switchMapотменяет ответ на клиенте, но не работу на сервере. Для POST и PUT это потерянные или задвоенные изменения, поэтому запись — черезconcatMapилиexhaustMap.mergeMapбез лимита конкурентности (mergeMap(fn, 4)) способен отправить сотни запросов разом.shareReplay(1)безrefCount: trueдержит подписку на источник и после ухода всех подписчиков.
Компромиссы
| Задача | Инструмент |
|---|---|
| состояние для шаблона | сигнал |
| загрузка данных по параметрам | httpResource, rxResource (стабильны с v22) |
| события во времени, WebSocket, debounce, retry | RxJS |
Как проверить
- Утечку видно в Chrome DevTools: снимок кучи после нескольких переходов туда и обратно показывает отсоединённые (detached) элементы и живые экземпляры компонента.
- Во вкладке Network отменённые запросы помечены
(canceled), а дубли запросов выдают лишнюю подписку.
Итог: RxJS — инструмент для потоков событий и конкурентности, а не для хранения состояния; состояние — в сигналах.
import { Component, inject } from '@angular/core';import { HttpClient } from '@angular/common/http';import { FormControl, ReactiveFormsModule } from '@angular/forms';import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';import { debounceTime, distinctUntilChanged, interval, switchMap } from 'rxjs';
@Component({ selector: 'app-product-search', imports: [ReactiveFormsModule], templateUrl: './product-search.html' })export class ProductSearch { private readonly http = inject(HttpClient); protected readonly query = new FormControl('', { nonNullable: true });
// switchMap отменяет прошлый запрос, если пользователь продолжил печатать protected readonly products = toSignal( this.query.valueChanges.pipe( debounceTime(300), distinctUntilChanged(), switchMap(q => this.http.get<{ id: number; name: string }[]>('/api/products', { params: { q } })), ), { initialValue: [] }, // отписка — автоматически при уничтожении компонента );
constructor() { // Бесконечный поток: без takeUntilDestroyed подписка переживёт компонент interval(60_000).pipe(takeUntilDestroyed()).subscribe(n => console.log('tick', n)); }}<input [formControl]="query" placeholder="Search" />@for (p of products(); track p.id) { <p>{{ p.name }}</p>}- Чем Observable отличается от Promise? — Ленивый, многозначный и отменяемый; Promise выполняется сразу и даёт одно значение.
- Какой оператор взять для кнопки «Сохранить»? —
exhaustMap, чтобы игнорировать повторные клики, илиconcatMap, если важен каждый клик по порядку. - Нужно ли отписываться от
HttpClient? — Он завершается сам, но отписка отменяет запрос и не даёт колбэку сработать на уничтоженном компоненте. - Чем
toSignalотличается отasyncpipe? — Даёт сигнал, который можно читать в коде и вcomputed; оба отписываются автоматически. - Почему
takeUntilставят последним? — Операторы после него, напримерswitchMap, могут держать внутреннюю подписку после сигнала остановки.
Что изменилось в Angular с версии 13 до актуальной?
Заголовок раздела «Что изменилось в Angular с версии 13 до актуальной?»Что это
Angular 13 вышел в ноябре 2021, текущая версия — 22 (июнь 2026). За это время сменился сам стиль кода.
Главное
- Компоненты без NgModule — standalone.
- Новый синтаксис шаблонов:
@if,@for,@switchвместо*ngIfи*ngFor. @defer— ленивая загрузка части шаблона.- Сигналы: состояние, входы
input()и выходыoutput(). inject()вместо параметров конструктора.- Сборка на esbuild и Vite — заметно быстрее Webpack.
- SSR с гидратацией — пакет
@angular/ssrвместо Angular Universal. - Работа без zone.js и OnPush по умолчанию.
Частая ошибка
Думать, что старый код перестал работать. NgModule, @Input() и *ngIf (deprecated с v20) поддерживаются, а ng update сам правит то, что ломается.
Когда что появилось
| Версия | Что изменилось |
|---|---|
| 14–15 (2022) | standalone-компоненты, типизированные формы, inject(), функциональные гарды и интерцепторы |
| 16 (май 2023) | сигналы (preview), takeUntilDestroyed, гидратация (preview), сборка на esbuild (preview) |
| 17 (ноябрь 2023) | @if, @for, @defer; стабильные сигналы; application builder и Vite для новых проектов |
| 17.1–19 | input(), output(), model(), сигнальные запросы; с v19 стабильны, standalone по умолчанию |
| 20 (май 2025) | стабильны effect, linkedSignal, toSignal; *ngIf и *ngFor deprecated; zoneless стабилен с 20.2 |
| 21 (ноябрь 2025) | zoneless и Vitest по умолчанию в новых проектах; экспериментальные Signal Forms |
| 22 (июнь 2026) | OnPush по умолчанию; стабильны Signal Forms, resource и httpResource; @Service(); HttpClient на fetch |
Ловушки миграции
ng updateпереходит только на одну мажорную версию за раз. С 13 до 22 — девять шагов, у каждого свои требования к Node.js и TypeScript.- Сторонние библиотеки обновляют вместе с Angular: Kendo UI for Angular, angular-oauth2-oidc и другие должны поддерживать целевую версию.
- Собственные настройки Webpack в application builder не переносятся.
Правило
Сначала довести версии до актуальной с сохранением поведения, потом модернизировать код миграциями ng generate @angular/core:... — по одной за раз.
Почему так
- Сигналы и zoneless убирают главную цену старого Angular — проверку всего дерева по любому событию.
- Standalone и
@deferделают единицей загрузки компонент, а не модуль. - esbuild и Vite ускоряют сборку; Webpack-сборщики в v22 объявлены deprecated.
- С v22 мажорный релиз выходит раз в 12 месяцев, а не раз в полгода. v23 ожидается около июня 2027.
Стратегия миграции
- Обновлять мажор за мажором по update guide, после каждого шага — сборка и тесты.
- Автоматические миграции сохраняют поведение: в v21 приложению добавляется
provideZoneChangeDetection(), в v22 компонентам без стратегии —ChangeDetectionStrategy.Eager. - Сборщик переводят командой
ng update @angular/cli --name use-application-builder. - Затем по одной: standalone, control flow,
inject(),input()иoutput(), ленивые маршруты. - Последними — OnPush и zoneless: они меняют логику, а не синтаксис.
Цена
- Каждая мажорная версия поднимает минимум TypeScript и Node.js: v22 требует TypeScript 6.0 и Node.js 22.22+ или 24.13+.
- Удаляются старые API: в v22 —
ComponentFactoryResolver.HttpClientперешёл на fetch, и для прогресса загрузки файлов нуженwithXhr(). - На октябрь 2026 Kendo UI for Angular 25.x поддерживает Angular 20–22, а angular-oauth2-oidc 22.x требует Angular 22.
Компромиссы
Переписывать всё сразу незачем: старый и новый стиль сосуществуют в одном приложении. Новое пишут в новом стиле, старое переводят при изменении.
Итог: миграция с 13 — сначала безопасное обновление версий с сохранением поведения, потом постепенный переход на сигналы и zoneless.
import { Component, EventEmitter, Input, Output, inject, input, output } from '@angular/core';import { HttpClient } from '@angular/common/http';
interface User { id: number; name: string; }
// Angular 13: декораторы, конструктор, объявление в declarations какого-нибудь NgModule@Component({ selector: 'app-user-old', templateUrl: './user-old.component.html' })export class UserOldComponent { @Input() user!: User; @Output() selected = new EventEmitter<User>(); constructor(private http: HttpClient) {}}
// Angular 22: standalone, сигнальные входы и выходы, inject()@Component({ selector: 'app-user', templateUrl: './user.html' })export class UserCard { readonly user = input.required<User>(); // читается как user() readonly selected = output<User>(); private readonly http = inject(HttpClient);}<!-- Angular 13 --><li *ngFor="let o of orders; trackBy: trackById">{{ o.title }}</li><p *ngIf="!orders.length">No orders</p>
<!-- Angular 22: track обязателен, @empty вместо отдельного *ngIf -->@for (o of orders(); track o.id) { <li>{{ o.title }}</li>} @empty { <p>No orders</p>}
<!-- Код графика загрузится, когда блок появится в области видимости -->@defer (on viewport) { <app-sales-chart />} @placeholder { <p>Chart</p>}- Можно ли смешивать NgModule и standalone? — Да: standalone-компонент импортируют в NgModule, а NgModule — в
importsкомпонента. - Что делает
trackв@for? — Задаёт ключ, по которому Angular сопоставляет элементы с DOM; в@forон обязателен. - Чем
@deferотличается от ленивого маршрута? — Откладывает загрузку части шаблона по триггеру (viewport, idle, interaction), а не целой страницы. - Почему нельзя обновиться с 13 сразу на 22? —
ng updateподдерживает переход только на одну мажорную версию, миграции рассчитаны на шаг. - Что такое гидратация? — Клиент переиспользует DOM, отрисованный на сервере, а не строит его заново.
Как строить большое Angular-приложение и управлять состоянием?
Заголовок раздела «Как строить большое Angular-приложение и управлять состоянием?»Что это
Большое приложение делят по фичам — бизнес-разделам: заказы, клиенты, отчёты.
core/— то, что нужно один раз: авторизация, интерцепторы, каркас страницы.shared/— общие UI-компоненты и пайпы без бизнес-логики.features/orders/— маршруты, страницы и состояние одной фичи.
Как пользоваться
Фичу подключают ленивым маршрутом: loadChildren: () => import('./orders/orders.routes'). Её код попадает в отдельный файл (чанк) и загружается при первом переходе.
Частая ошибка
Статически импортировать компонент ленивой фичи в другом месте, например в app.ts. Тогда он попадёт в основной бандл, и ленивая загрузка пропадёт.
Когда что выбирать
| Подход к состоянию | Когда |
|---|---|
| сервис с сигналами | состояние одной фичи, простые операции |
| NgRx SignalStore | много сторов, нужна единая структура и плагины: withEntities, события |
| NgRx Store | общее состояние многих фич, сложные потоки событий, Redux DevTools |
Производительность
- OnPush и сигналы — меньше проверок.
trackпоidв@for— DOM переиспользуется, а не пересоздаётся.- Длинные списки — виртуальная прокрутка:
cdk-virtual-scroll-viewportиз Angular CDK илиscrollable="virtual"в Kendo Grid. - Тяжёлые виджеты — графики, планировщик — в
@defer.
Ловушки
providedIn: 'root'для состояния страницы: оно переживает уход со страницы, и при возврате видны старые данные. Такой стор объявляют вprovidersмаршрута или компонента.- Глобальный стор для всего подряд, включая состояние одной формы.
- Kendo Grid на тысячи строк без пейджинга или виртуальной прокрутки.
Правило
Начинать с сервиса на сигналах. SignalStore — когда сторов много и нужна единая форма, NgRx Store — когда состояние действительно общее и сложное.
Компромиссы в состоянии
- Большая часть «состояния» — кеш серверных данных. Его закрывают
httpResourceили сервис; глобальный стор нужен для общего: пользователь, права, настройки. - NgRx Store даёт трассировку и time travel в Redux DevTools ценой шаблонного кода: action, reducer, selector и effect на операцию.
providersмаршрута дают стору время жизни фичи — как scope в ASP.NET Core, только на раздел приложения.
Цена размера бандла
budgetsвangular.jsonвалят сборку, если начальный бандл превысил лимит.- Состав бандла показывают
ng build --stats-jsonи анализатор esbuild. - Kendo UI и Angular Material добавляют вес: импортируют только нужные компоненты, а Chart и Scheduler прячут за
@defer.
Библиотеки компонентов
- Обновление Angular ограничено их
peerDependencies— проверяют доng update. - Своя обёртка вроде
app-data-gridповерх Kendo Grid локализует настройки и будущую замену библиотеки.
Авторизация в SPA
Функциональный интерцептор добавляет access-токен, гард canMatch скрывает недоступные маршруты. Гард — удобство для пользователя, права проверяет API. Где хранить токены и когда нужен BFF — в теме OAuth 2.0 и OpenID Connect.
Как проверить в проде
Core Web Vitals реальных пользователей и Lighthouse показывают итог, вкладка Coverage в Chrome — сколько загруженного JavaScript не выполнилось.
Итог: масштаб держат границы фич, ленивая загрузка и осознанный выбор, где живёт состояние.
import { Routes } from '@angular/router';import { inject } from '@angular/core';import { AuthService } from './core/auth.service';import { OrdersStore } from './features/orders/orders.store';
export const routes: Routes = [ { path: '', loadComponent: () => import('./features/home/home').then(m => m.Home) }, { path: 'orders', canMatch: [() => inject(AuthService).isLoggedIn()], // удобство; права проверяет API providers: [OrdersStore], // один стор на фичу, живёт, пока открыт раздел // Отдельный чанк: загрузится при первом переходе на /orders loadChildren: () => import('./features/orders/orders.routes').then(m => m.ORDERS_ROUTES), },];import { computed, inject } from '@angular/core';import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';import { Order, OrdersApi } from './orders.api';
export const OrdersStore = signalStore( withState({ orders: [] as Order[], filter: '', loading: false }), withComputed(({ orders, filter }) => ({ // Производное состояние — обычный computed visible: computed(() => orders().filter(o => o.customer.includes(filter()))), })), withMethods((store, api = inject(OrdersApi)) => ({ setFilter(filter: string) { patchState(store, { filter }); }, async load() { patchState(store, { loading: true }); patchState(store, { orders: await api.loadAll(), loading: false }); }, })),);- Где объявить стор страницы? — В
providersмаршрута или компонента, чтобы он умирал вместе с разделом. - Когда NgRx Store оправдан? — Много фич делят сложное состояние, нужны строгий однонаправленный поток и Redux DevTools.
- Почему импорт из ленивой фичи может сломать ленивую загрузку? — Статический импорт тянет модуль в основной бандл.
- Защищает ли гард данные? — Нет, это UX: код SPA доступен пользователю, права проверяет API.
- Как ускорить таблицу на 10 000 строк? — Пейджинг или виртуальная прокрутка, OnPush и
trackпоid.