Компоненты и шаблоны
Что это такое
Компонент Angular — это класс на TypeScript с декоратором, который указывает ещё и на HTML-шаблон с таблицей стилей. Данные из класса шаблон читает через собственный синтаксис привязок и через него же сообщает события обратно, а обнаружение изменений — та работа фреймворка, которая решает, когда привязки шаблона нужно пересчитать.
Почему это важно
Модель компонентов есть у каждого фронтенд-фреймворка, но модель Angular отличается тем, насколько она явная и типизированная. Шаблоны компилируются отдельно, поэтому ошибки привязок ловятся на сборке, а не во время выполнения, и компилятор может сказать, что свойство, на которое вы сослались, больше не существует. Именно эта надёжность объясняет, почему фреймворк популярен в регулируемых и долгоживущих проектах. Модель обнаружения изменений объясняет и поведение по производительности: дорогой шаблон делает страницу медленной, однако в коде компонента причину не видно — чтобы её увидеть, нужно понимать, когда запускается проверка.
Ключевые идеи
Декоратор объявляет контракт компонента
Декоратор @Component называет селектор — тег, которым пользуются компонент, — шаблон, стили и способ обнаружения изменений. С этими метаданными работают и компилятор, и внедритель зависимостей, поэтому и записаны они над классом, а не внутри него.
import { Component, Input } from "@angular/core";
@Component({
selector: "app-badge",
template: `<span class="badge">{{ label }}</span>`,
styles: [".badge { padding: 0.25rem 0.5rem; }"],
})
export class BadgeComponent {
@Input({ required: true }) label!: string;
}
Привязки читают скобками и слушают круглыми скобками
[property]="expression" задаёт свойство DOM или вход компонента; (event)="handler()" слушает событие; [(ngModel)] объединяет и то и другое для элемента формы. Пунктуация и есть синтаксис: прочитайте её вслух — «свойство из выражения», «событие в обработчик» — и направление потока данных станет видно сразу.
<input [(ngModel)]="query" (keyup.enter)="search()" />
<p *ngIf="results.length === 0">Совпадений нет.</p>
<ul>
<li *ngFor="let item of results; trackBy: trackById">{{ item.name }}</li>
</ul>
Структурные директивы меняют DOM
Звёздочка обозначает сокращение, которое оборачивает элемент в шаблон: *ngIf добавляет и удаляет его, *ngFor повторяет. Новый блочный синтаксис @if и @for яснее по форме и лучше сужает типы внутри блока.
@for (item of results; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>Совпадений нет.</li>
}
Обнаружение изменений ищет изменения, а OnPush сужает поиск
По умолчанию Angular проверяет привязки компонента всякий раз, когда хоть что-то могло измениться. Объявление ChangeDetectionStrategy.OnPush сужает проверку до двух случаев: изменилась ссылка на вход или внутри компонента сработало событие. Выигрыш по производительности большой, однако вместе с ним приходит дисциплина: измените входной объект на месте — и представление перестанет обновляться.
Хуки жизненного цикла — это шов для побочных эффектов
ngOnInit выполняется после того, как входы впервые заданы, ngOnDestroy — прежде чем компонент исчезнет. Работа, которая должна произойти один раз, — запрос, подписка, — принадлежит первому, а её очистка — второму.
Практика
Соберите типизированный презентационный компонент
Создайте компонент, который принимает один типизированный вход и испускает одно событие, и используйте его из родителя, включая случай с пустым списком.
Готово, когда: компилятор отвергает неизвестное имя входа, полезная нагрузка события типизирована у родителя, а сам компонент не держит собственного состояния.
Переведите компонент на OnPush и сломайте его
Включите OnPush у компонента, которому родитель передаёт массив и изменяет его на месте, а не заменяет.
Готово, когда: вы увидели, что представление перестало обновляться, и можете назвать однострочное изменение у родителя, которое это исправляет.
Частые ошибки
- Изменение входного объекта на месте при
OnPush— ссылка не меняется, поэтому представление не перепроверяется; исправить: заменяйте объект, а не правьте его. - Вызов тяжёлого метода в привязке — он выполняется на каждом проходе обнаружения изменений; исправить: вычислите значение в поле или пайпе и привязывайтесь к нему.
- Подписка в конструкторе — компонент ещё не инициализирован, а входы не заданы; исправить: подписывайтесь в
ngOnInitи отписывайтесь вngOnDestroy. - Отсутствие
trackByу повторяемого списка — фреймворк пересоздаёт все строки при любом изменении; исправить: отслеживайте по стабильному id. - Использование
any, чтобы угодить привязке шаблона — главное преимущество компилятора отброшено; исправить: типизируйте поле, чтобы привязка проверялась.
Что читать дальше
- TypeScript (02-frontend/19-typescript) — декораторы, дженерики и сужение типов постоянно используются в API Angular.
- Внедрение зависимостей и RxJS (02-frontend/30-angular/02-dependency-injection-and-rxjs) — как сервисы попадают в компоненты и как составляются асинхронные данные.
- Управление состоянием (02-frontend/23-state-management) — где кончается состояние компонента и начинается сервис или хранилище.