Angular — корпоративный framework
Angular — зрелый фреймворк от Google, построенный на TypeScript: компоненты, зависимости, маршрутизация и HTTP-клиент собраны в один инструмент. В этом уроке ты создашь приложение через Angular CLI, свяжешь сервисы с данными и соберёшь продакшен-версию.
1. Что такое Angular и почему TypeScript
Angular — фреймворк «всё включено»: разметка, формы, HTTP, маршрутизация и тесты на месте, без выбора сторонних библиотек. Он написан на TypeScript, поэтому код с первого дня получает проверку типов, автодополнение и рефакторинг в редакторе.
// компонент Angular выглядит так
import { Component } from '@angular/core'
@Component({
selector: 'app-hello',
standalone: true,
template: '<h1>Привет, {{ name }}!</h1>'
})
export class HelloComponent {
name = 'мир'
}2. Angular CLI и структура проекта
Angular CLI генерирует проект, сервер разработки, компоненты и сборку. Работа с ним — это почти всегда команды одного инструмента.
npm install -g @angular/cli
ng new my-app --style=scss --routing
cd my-app
ng serve
# приложение откроется на http://localhost:4200ng generate component product-card
ng generate service data
ng generate interface productmy-app/
├── src/
│ ├── app/
│ │ ├── app.component.ts
│ │ ├── app.routes.ts
│ │ └── product-card/
│ ├── index.html
│ ├── main.ts
│ └── styles.scss
├── angular.json // конфигурация проекта
└── tsconfig.json3. Компоненты и шаблоны
Компонент — это класс с декоратором @Component. Шаблон связывает данные через интерполяцию {{ }}, а свойства привязываются квадратными скобками [value], события — круглыми (click).
// product-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core'
@Component({
selector: 'app-product-card',
standalone: true,
templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
@Input() title = ''
@Output() buy = new EventEmitter<string>()
onBuy() {
this.buy.emit(this.title)
}
}<!-- product-card.component.html -->
<div class="card">
<h2>{{ title }}</h2>
<button (click)="onBuy()">Купить</button>
</div><!-- использование в родителе -->
<app-product-card [title]="product.name"
(buy)="addToCart($event)" />*ngIf), а сложные вычисления выноси в методы или pipes. Раздутые шаблоны тяжело читать и тестировать.4. Директивы
Структурные директивы меняют DOM: *ngIf рисует по условию, *ngFor повторяет разметку. Атрибутные директивы (ngClass, ngStyle) просто меняют свойства элемента.
<ul>
<li *ngFor="let item of items; let i = index">
{{ i + 1 }}. {{ item.name }}
</li>
</ul>
<p *ngIf="items.length === 0; else emptyBlock">Список пуст</p>
<ng-template #emptyBlock>
<p>Добавьте первый товар</p>
</ng-template>// Атрибутная привязка классов
<div [ngClass]="{
'is-active': active,
'is-loading': loading
}">Статус</div>@for (item of items; track item.id) — она быстрее и короче, чем классический *ngFor.5. Pipes: форматирование данных
Pipes превращают данные перед показом: даты, деньги, строки. Встроенных хватает для большинства задач, а свои пишутся декоратором @Pipe.
<p>Цена: {{ price | currency: 'RUB' }}</p>
<p>Дата: {{ createdAt | date: 'd MMM yyyy' }}</p>
<p>Текст: {{ title | uppercase }}</p>// Собственный pipe — сокращение числа
import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ name: 'shortNumber', standalone: true })
export class ShortNumberPipe implements PipeTransform {
transform(value: number): string {
if (value >= 1000) return (value / 1000).toFixed(1) + 'k'
return String(value)
}
}6. Сервисы и dependency injection
Сервис — класс с бизнес-логикой и данными, доступными всем компонентам. Angular сам создаёт и передаёт его через dependency injection: достаточно добавить в конструктор тип.
@Injectable({ providedIn: 'root' })
export class CartService {
private items: string[] = []
add(item: string) {
this.items.push(item)
}
getItems() {
return this.items
}
count() {
return this.items.length
}
}import { Component } from '@angular/core'
import { CartService } from '../services/cart.service'
@Component({
selector: 'app-header',
standalone: true,
template: '<p>В корзине: {{ cart.count() }}</p>'
})
export class HeaderComponent {
constructor(private cart: CartService) {}
}providedIn: 'root' делает сервис одиночкой для всего приложения — одно и то же состояние корзины видно во всех компонентах.7. RxJS и потоковые данные
RxJS — библиотека реактивных потоков: события, запросы и обновления данных — всё это Observable. Angular использует RxJS активно: формы, HTTP и роутер работают на потоках.
import { Subject } from 'rxjs'
@Injectable({ providedIn: 'root' })
export class SearchService {
private search = new Subject<string>()
setQuery(value: string) {
this.search.next(value)
}
query$ = this.search.asObservable()
}import { Component, inject } from '@angular/core'
import { AsyncPipe } from '@angular/common'
import { distinctUntilChanged } from 'rxjs'
@Component({
standalone: true,
imports: [AsyncPipe],
template: '<p>Запрос: {{ query$ | async }}</p>'
})
export class SearchComponent {
private search = inject(SearchService)
query$ = this.search.query$.pipe(distinctUntilChanged())
}// Горячие клавиши средствами RxJS
import { fromEvent } from 'rxjs'
import { map } from 'rxjs'
const esc$ = fromEvent<KeyboardEvent>(document, 'keydown')
.pipe(map((e) => e.key))
// сюда можно подписаться и закрыть модальное окно| Оператор | Что делает |
|---|---|
| map | преобразует значение |
| filter | пропускает неугодные значения |
| debounceTime | ждёт паузу между событиями |
| distinctUntilChanged | убирает повторы |
| switchMap | переключает на последний поток |
| catchError | обрабатывает ошибку |
| async или takeUntilDestroyed(), иначе потоки продолжат жить после уничтожения компонента.8. Reactive Forms: формы без боли
Реактивные формы описывают состояние в коде: FormGroup собирает поля, FormControl хранит значение и валидность. Удобны для сложной валидации и динамических форм.
import { Component, inject } from '@angular/core'
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" placeholder="Email">
<input formControlName="password" type="password" placeholder="Пароль">
<button type="submit" [disabled]="form.invalid">Войти</button>
</form>`
})
export class LoginComponent {
private fb = inject(NonNullableFormBuilder)
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
})
onSubmit() {
console.log('Вход:', this.form.value)
}
}// Проверка поля после касания
<p *ngIf="form.controls.email.invalid && form.controls.email.touched">
Введите корректный email
</p>9. Роутинг и ленивая загрузка
Роутер сопоставляет адреса и компоненты. Секции приложения стоит грузить лениво — с помощью loadComponent, чтобы первый экран открывался быстро.
// app.routes.ts
import { Routes } from '@angular/router'
import { provideRouter } from '@angular/router'
import { bootstrapApplication } from '@angular/platform-browser'
export const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'catalog',
loadComponent: () => import('./catalog/catalog.component')
.then((m) => m.CatalogComponent)
},
{ path: '**', redirectTo: '' }
]// ссылки в шаблоне
<nav>
<a routerLink="/" routerLinkActive="active">Главная</a>
<a routerLink="/catalog">Каталог</a>
</nav>
<router-outlet></router-outlet>| Конструкция | Назначение |
|---|---|
| routerLink | навигация по приложению |
| routerLinkActive | класс активного пункта меню |
| router-outlet | место для компонента страницы |
| loadComponent | ленивая загрузка модуля |
| canActivate | защита маршрута |
{ path: 'catalog/:id', component: ... }, а в компоненте — через inject(ActivatedRoute).snapshot.params['id'].10. HttpClient и продакшен-сборка
Общение с бэкендом идёт через HttpClient: методы возвращают Observable, а async pipe сам отписывается от потока при уничтожении компонента.
import { HttpClient } from '@angular/common/http'
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient)
getUsers() {
return this.http.get<User[]>('/api/users')
}
createUser(user: Partial<User>) {
return this.http.post<User>('/api/users', user)
}
}// компонент отдаёт данные в шаблон через async
users$ = this.api.getUsers()
// в HTML: <li *ngFor="let u of users$ | async">{{ u.name }}</li>ng build --configuration production
# собранные файлы лежат в dist/
ng serve --host 0.0.0.0 --port 4200// сервер разработки (для локального бэкенда)
npm install -g json-server
json-server --watch db.json --port 3000dist/ на любой хостинг или CDN.11. Хранение состояния: сигналы и сервисы
Angular предлагает разные способы хранить данные между компонентами: сервисы с @Injectable, RxJS-потоки и современные сигналы (signals). Сигналы появились в Angular 16+ и сделали реактивность проще и легче.
import { Injectable, signal, computed } from '@angular/core'
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<string[]>([])
readonly count = computed(() => this.items().length)
add(item: string) {
this.items.update((prev) => [...prev, item])
}
}В шаблоне сигнал читается без подписки, а изменения обновляют DOM автоматически:
<p>В корзине: {{ cart.count() }}</p>| Подход | Когда выбирать |
|---|---|
| сервис с полями | простые случаи, данные без реактивности |
| BehaviorSubject/поток | множество подписчиков, комбинирование потоков |
| сигналы | новые проекты, явная реактивность, лёгкий трекинг |
12. Interceptors и токены авторизации
HttpInterceptor перехватывает каждый запрос и может добавить заголовки, логировать ошибки или повторять запрос после 401. Это чистый способ работы с авторизацией.
import { HttpInterceptorFn } from '@angular/common/http'
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('token')
if (token) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
})
}
return next(req)
}Подключение в поставщиках приложения:
providers: [
provideHttpClient(
withInterceptors([authInterceptor])
)
]- не храните токены в обычном локальном хранилище без шифрования для публичных приложений;
- обработка 401 — перехватчик выходит на первичное «логин-окно»;
- логирование и Sentry также удобно делать через interceptors.
13. Change Detection: фишки производительности
Angular по умолчанию проверяет компоненты после каждого события. На больших страницах это отдаёт расходы: примените стратегию OnPush — компонент перерисуется только при изменении его входов или сигналов.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<p>{{ item.name }}</p>'
})
export class ItemComponent {
@Input() item!: Item
}- OnPush изолирует перерисовку: контент меняется реже и быстрее;
- не мутируйте объекты «на месте» — создавайте новые ссылки, иначе OnPush не увидит изменения;
- списки с
trackByуменьшают работу при обновлении коллекций.
<li *ngFor="let item of items; trackBy: trackById">
{{ item.id }}: {{ item.name }}
</li>14. Angular и серверный рендеринг (SSR)
Для SEO и скорости первой загрузки Angular умеет рендерить страницы на сервере через Angular Universal, используя провайдер provideServerRendering. Приложение выполняется на Node.js, отдаёт готовый HTML, затем «гидрация» берёт управление.
ng add @angular/ssr
# после установки:
ng build # собираются клиентская и серверная частиПреимущества:
- первое содержимое появляется быстрее: не требуется загрузка всего бандла;
- роботы поисковиков видят реальный контент без выполнения JS;
- годится и пререндер для статических страниц.
window и document на сервере недоступны. Работайте с ними только в клиентских хуках (ngAfterViewInit, effect).15. Тестирование Angular-компонентов
Тесты защищают приложение от регрессий: вы меняете один компонент, а тесты ловят, что задел другой. Для Angular стандарт — Jasmine + Karma или современный Vitest + Testing Library.
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AppComponent]
});
});
it('отображает заголовок', () => {
const fixture = TestBed.createComponent(AppComponent);
const el = fixture.nativeElement.querySelector('h1');
expect(el.textContent).toContain('Привет');
});
});- юнит-тесты: логика сервисов и трансформации данных без UI;
- компонентные: шаблон, вывод, события кликов;
- интеграционные: общение компонентов и DI;
- E2E (Playwright/Cypress): критичные сценарии пользователя.
16. Аутентификация и JWT
Большинство приложений требует входа: вы регистрируетесь, получаете токен и прикладываете его к каждому запросу. В Angular это делают Interceptor и сервис хранения сессии.
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
const token = sessionStorage.getItem('token');
const clone = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next.handle(clone);
}
}- JWT состоит из header.payload.signature — подпись проверяет сервер;
- хранить токен: HttpOnly cookie для SPA или security-context подходы;
- при 401 обновляйте через refresh-токен, не заставляйте логиниться заново;
- Role-защита маршрутов через Guard дополнительно к проверке на сервере.
localStorage доступен любому скрипту на странице — при XSS он утечёт. HttpOnly-куки безопаснее для чувствительных данных.17. Angular Material и UI-библиотеки
Писать UI с нуля долго и дорого: быстрее взять проверенные компоненты. Angular Material — официальный набор с таблицами, формами, диаграммами и доступностью из коробки.
npm install @angular/material @angular/cdk
// app.module.ts
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
imports: [MatTableModule, MatSortModule]| Задача | Компонент |
|---|---|
| список с сортировкой | MatTable + MatSort |
| формы и валидация | MatFormField + MatInput |
| уведомления | MatSnackBar |
| диалоги | MatDialog |
18. Оптимизация сборки и Lighthouse
Скорость загрузки — метрика, которую видит каждый пользователь. Angular даёт «дешёвые» победы: ленивая загрузка, предзагрузка стратегий, бюджеты бандла и проверка через Lighthouse.
- ленивые маршруты:
loadChildren: () => import('./x.module'); - preload-стратегия:
PreloadAllModules— загружайте в фоне после открытия; - budget в angular.json:
"budgets": [{ "type": "initial", "maximumWarning": "1mb" }]; - прогоните Lighthouse: LCP, CLS, blocking resources;
- Include
deferна скрипты не критичных виджетов.
// preloading стратегия
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})19. Межкомпонентное взаимодействие через сервисы
Прямые связи «родитель — ребёнок» быстро становятся хаосом при росте приложения. Общий сервис с сигналами (signals) становится единым источником правды для состояния экрана.
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<Item[]>([]);
readonly count = computed(() => this.items().length);
add(item: Item) { this.items.update(list => [...list, item]); }
}- сервис в корне — один инстанс на всё приложение;
- сигналы перекомпоновывают зависимости реактивно, без подписок;
- минимум прямых сервис-зависимостей между фичами;
- состояние храните в domain-слое, а не в компоненте.
20. Angular Elements: компоненты вне приложения
Иногда компонент Angular нужно использовать вне приложения: в другом фреймворке или на статичной странице. Angular Elements превращает компонент в Web Component, работающий где угодно.
const el = createCustomElement(MyWidget, { injector: envInjector });
customElements.define('my-widget', el);- Web Components совместимы с React, Vue и чистым HTML;
- каждый элемент несёт свой Shadow DOM — стили изолированы;
- входные данные — через атрибуты, события — через Output;
- типичные кейсы: виджеты аналитики, рекламные блоки, мини-формы.
21. Миграция со старых версий Angular
Переход с AngularJS или старой версии Angular — проект на месяцы без стратегии. Разбивка: параллельная работа старого и нового, перенос «экран за экраном», границы через роутер.
- ngUpgrade позволяет гибридный режим AngularJS внутри нового приложения;
- переносите функциональность целыми экранами, а не отдельными компонентами;
- покрывайте переносимые экраны тестами до старта работ;
- обновления зависимостей — отдельные вехи со своим списком.
22. Роутинг: guards, resolvers и ленивая загрузка
Роутер — не только навигация: через guards вы ограничиваете доступ, через resolvers подгружаете данные до рендера, через ленивую загрузку — шкалируете размер бандла.
- CanActivate проверяет права до входа на маршрут;
- Resolver получает данные ДО рендера страницы — экран не «моргает»;
- loadChildren откладывает загрузку модуля до перехода по маршруту;
- PreloadAllModules — предзагрузка в фоне после старта.
{
path: 'admin', loadChildren: () => import('./admin.routes'),
canActivate: [AuthGuard]
}Что дальше
Ты разобрался в основах Angular: компоненты, директивы, сервисы, RxJS, формы и роутинг. Собери каталог товаров с фильтром и корзиной, чтобы связать все темы вместе. Для уверенности продолжи погружение в TypeScript и работу с бэкендом через REST API.