Angular — корпоративный framework

Angular — зрелый фреймворк от Google, построенный на TypeScript: компоненты, зависимости, маршрутизация и HTTP-клиент собраны в один инструмент. В этом уроке ты создашь приложение через Angular CLI, свяжешь сервисы с данными и соберёшь продакшен-версию.

Уровень: средний 18 разделов 3–4 часа

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 = 'мир'
}
Модульная модель Angular (NgModule) уже опциональна: современные standalone-компоненты объявляют свои зависимости сами, без лишних обвязок.

2. Angular CLI и структура проекта

Angular CLI генерирует проект, сервер разработки, компоненты и сборку. Работа с ним — это почти всегда команды одного инструмента.

npm install -g @angular/cli
ng new my-app --style=scss --routing
cd my-app
ng serve
# приложение откроется на http://localhost:4200
ng generate component product-card
ng generate service data
ng generate interface product
my-app/
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.routes.ts
│   │   └── product-card/
│   ├── index.html
│   ├── main.ts
│   └── styles.scss
├── angular.json      // конфигурация проекта
└── tsconfig.json
Имена файлов CLI всегда в kebab-case (product-card), а классы — в PascalCase (ProductCardComponent). Следуй генератору — он ставит правильные имена автоматически.

3. Компоненты и шаблоны

Компонент — это класс с декоратором @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>
Цикл в Angular 17+ можно писать через директиву @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)
  }
}
Пайпы должны быть чистыми функциями: без мутаций входных данных и побочных эффектов. Иначе Angular не сможет безопасно переиспользовать их при change detection.

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>
Реактивные формы проще тестировать: состояние не спрятано в DOM, а лежит в объектах, которые можно проверять в юнит-тестах без рендеринга.

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 3000
Продакшен-сборка включает AOT-компиляцию, минификацию и tree-shaking. Собранный сайт деплой — это просто статические файлы из папки dist/ на любой хостинг или CDN.
Для имитации бэкенда во время обучения удобен json-server: он отдаёт REST API из локального JSON-файла и не требует реального сервера.

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/потокмножество подписчиков, комбинирование потоков
сигналыновые проекты, явная реактивность, лёгкий трекинг
Начинайте с сигналов: они легче в чтении и лучше интегрируются с zoneless-режимом будущих версий Angular.

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])
  )
]
Взаимодействие с бэкендом по настоящему защищённому токену — устраивайте обмен по HTTPS в продакшене. Храните секреты на сервере, не в клиенте.

13. Change Detection: фишки производительности

Angular по умолчанию проверяет компоненты после каждого события. На больших страницах это отдаёт расходы: примените стратегию OnPush — компонент перерисуется только при изменении его входов или сигналов.

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: '<p>{{ item.name }}</p>'
})
export class ItemComponent {
  @Input() item!: Item
}
<li *ngFor="let item of items; trackBy: trackById">
  {{ item.id }}: {{ item.name }}
</li>
TrackBy помогает Angular не пересоздавать DOM-элементы, а переиспользовать их — это заметно на длинных списках.

14. Angular и серверный рендеринг (SSR)

Для SEO и скорости первой загрузки Angular умеет рендерить страницы на сервере через Angular Universal, используя провайдер provideServerRendering. Приложение выполняется на Node.js, отдаёт готовый HTML, затем «гидрация» берёт управление.

ng add @angular/ssr
# после установки:
ng build   # собираются клиентская и серверная части

Преимущества:

SSR усложняет работу с браузерными API: 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('Привет');
  });
});
Покрывайте тестами то, что меняется чаще всего: формы, сервисы загрузки данных и публичные методы компонентов. Не гонитесь за 100% ради цифры.

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);
  }
}
Токен в 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
Подключайте только нужные модули: tree-shaking оставит в бандле лишь используемые. Это сильно экономит размер сборки для серьёзного проекта.

18. Оптимизация сборки и Lighthouse

Скорость загрузки — метрика, которую видит каждый пользователь. Angular даёт «дешёвые» победы: ленивая загрузка, предзагрузка стратегий, бюджеты бандла и проверка через Lighthouse.

  1. ленивые маршруты: loadChildren: () => import('./x.module');
  2. preload-стратегия: PreloadAllModules — загружайте в фоне после открытия;
  3. budget в angular.json: "budgets": [{ "type": "initial", "maximumWarning": "1mb" }];
  4. прогоните Lighthouse: LCP, CLS, blocking resources;
  5. Include defer на скрипты не критичных виджетов.
// preloading стратегия
RouterModule.forRoot(routes, {
  preloadingStrategy: PreloadAllModules
})
Показатель Lighthouse зависит от окружения: медленная машина и слабый Wi-Fi меняют цифры. Замеряйте на реальных устройствах, а не только в десктопном Chrome.

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]); }
}
Начните с одного «store»-сервиса для корзины или пользователя: паттерн понятный и легко тестируется.
Не держите всё состояние в одном гигантском сервисе: разделяйте по доменам, иначе через год никто не разберётся в зависимостях.

20. Angular Elements: компоненты вне приложения

Иногда компонент Angular нужно использовать вне приложения: в другом фреймворке или на статичной странице. Angular Elements превращает компонент в Web Component, работающий где угодно.

const el = createCustomElement(MyWidget, { injector: envInjector });
customElements.define('my-widget', el);
Не стройте «весь сайт на элементах»: это инструмент для кирпичиков внутри чужих страниц, а не каркас приложения.
Angular Elements тянет рантайм Angular: перед использованием оцените размер бандла для маленьких виджетов.

21. Миграция со старых версий Angular

Переход с AngularJS или старой версии Angular — проект на месяцы без стратегии. Разбивка: параллельная работа старого и нового, перенос «экран за экраном», границы через роутер.

Сначала обновите инструменты (CLI, Node, TypeScript), затем код: так меньше конфликтов между версиями.
Миграция без плана «что в каждый спринт» растягивается навсегда: фиксируйте границу готового и неготового.

22. Роутинг: guards, resolvers и ленивая загрузка

Роутер — не только навигация: через guards вы ограничиваете доступ, через resolvers подгружаете данные до рендера, через ленивую загрузку — шкалируете размер бандла.

{
  path: 'admin', loadChildren: () => import('./admin.routes'),
  canActivate: [AuthGuard]
}
Resolvers подходят для данных, нужных сразу; тяжёлые данные грузите после рендера, чтобы не блокировать маршрут.
Храните guards в отдельном домене: переиспользуемые проверки проще тестировать и менять централизованно.

Что дальше

Ты разобрался в основах Angular: компоненты, директивы, сервисы, RxJS, формы и роутинг. Собери каталог товаров с фильтром и корзиной, чтобы связать все темы вместе. Для уверенности продолжи погружение в TypeScript и работу с бэкендом через REST API.

← Вернуться к курсам