3. Первое приложение
🏗️ Анатомия первого Angular приложения
Заголовок раздела «🏗️ Анатомия первого Angular приложения»Ты создал проект командой ng new, сервер запущен, браузер открыт — и перед тобой стандартная страница Angular. Но что именно происходит? Как Angular запускается? Кто рендерит этот HTML? Давай разберём всё по косточкам! 🦴
🎭 Декораторы: магия TypeScript
Заголовок раздела «🎭 Декораторы: магия TypeScript»Прежде чем разбирать компоненты — нужно понять декораторы. Это TypeScript фича, на которой держится весь Angular.
Декоратор — это функция, которая добавляет метаданные к классу, методу или свойству. Выглядит как @НазваниеДекоратора.
// Обычный класс TypeScript — просто классclass Car { speed = 0; accelerate() { this.speed += 10; }}
// Тот же класс с декоратором — теперь Angular знает что это компонент!@Component({ selector: 'app-car', template: '<div>Speed: {{ speed }}</div>'})class Car { speed = 0; accelerate() { this.speed += 10; }}Как работает декоратор под капотом:
// @Component — это просто функция, которую вызывают с конфигом// и которая "украшает" класс метаданнымиfunction Component(config: ComponentConfig) { return function(target: any) { // Angular сохраняет метаданные через Reflect Reflect.defineMetadata('annotations', [config], target); return target; };}
// @Component({ selector: 'app-root', ... })// эквивалентно вызову: Component({ selector: 'app-root', ... })(AppComponent)Основные декораторы Angular:
@Component({...}) // Объявляет класс компонентом@Directive({...}) // Объявляет класс директивой@Pipe({...}) // Объявляет класс пайпом@Injectable({...}) // Позволяет внедрять класс через DI@NgModule({...}) // Объявляет класс Angular модулем@Input() // Свойство получает данные от родителя@Output() // Событие отправляет данные родителю@HostListener(...) // Слушает события на host элементе@ViewChild(...) // Ссылка на дочерний компонент/элемент🧩 Анатомия @Component
Заголовок раздела «🧩 Анатомия @Component»Компонент — это сердце Angular. Каждый UI элемент — это компонент. Кнопка, форма, страница, шапка, подвал — всё компоненты.
import { Component, Input, OnInit } from '@angular/core';import { CommonModule } from '@angular/common';
@Component({ // ① Селектор — имя HTML тега для этого компонента selector: 'app-user-card',
// ② Шаблон — можно inline или ссылка на файл templateUrl: './user-card.component.html', // ЛИБО inline: // template: `<div>{{ user.name }}</div>`,
// ③ Стили — можно массив файлов или inline styleUrls: ['./user-card.component.scss'], // ЛИБО inline: // styles: [`h2 { color: red; }`],
// ④ Standalone — не нужен NgModule (Angular 14+) standalone: true,
// ⑤ Импорты (только для standalone) imports: [CommonModule],
// ⑥ Стратегия Change Detection changeDetection: ChangeDetectionStrategy.OnPush,})export class UserCardComponent implements OnInit { // ⑦ @Input — принимаем данные от родителя @Input() user!: { name: string; email: string; role: string }; @Input() isHighlighted = false;
// ⑧ @Output — отправляем события родителю @Output() userSelected = new EventEmitter<User>();
// ⑨ Приватные поля (не видны в шаблоне) private clickCount = 0;
// ⑩ Публичные поля (видны в шаблоне) isExpanded = false; formattedDate = '';
// ⑪ Конструктор — только для инъекции зависимостей! constructor(private userService: UserService) {}
// ⑫ Lifecycle хуки ngOnInit() { // Инициализация данных — здесь, не в конструкторе this.formattedDate = new Date().toLocaleDateString('ru'); }
// ⑬ Методы компонента toggleExpand() { this.isExpanded = !this.isExpanded; this.clickCount++; }
onSelect() { this.userSelected.emit(this.user); }}Шаблон компонента:
<div class="card" [class.highlighted]="isHighlighted" (click)="toggleExpand()">
<!-- Интерполяция — вывод переменных --> <h2>{{ user.name }}</h2> <p>{{ user.email }}</p>
<!-- Property binding — привязка к атрибуту --> <span [class]="'role-badge ' + user.role">{{ user.role }}</span>
<!-- Event binding — обработка событий --> <button (click)="onSelect(); $event.stopPropagation()">Выбрать</button>
<!-- Директива *ngIf — условный рендеринг --> <div *ngIf="isExpanded" class="details"> Добавлен: {{ formattedDate }} </div>
</div>📋 selector: как использовать компонент
Заголовок раздела «📋 selector: как использовать компонент»Поле selector определяет как Angular находит место в HTML для компонента:
// Вариант 1: тег (самый частый)selector: 'app-user-card'// Использование: <app-user-card [user]="currentUser" />
// Вариант 2: атрибутselector: '[appHighlight]'// Использование: <div appHighlight>...</div>
// Вариант 3: класс CSS (редко)selector: '.app-special'// Использование: <div class="app-special">...</div>Соглашение: Angular CLI использует префикс
app-по умолчанию. Для библиотек — свой префикс (напримерmat-buttonу Angular Material).
📄 templateUrl vs template
Заголовок раздела «📄 templateUrl vs template»// ✅ Отдельный файл — для сложных шаблонов (рекомендуется)@Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', // путь относительно .ts файла})
// ✅ Inline шаблон — для простых компонентов@Component({ selector: 'app-spinner', template: ` <div class="spinner"> <div class="bounce1"></div> <div class="bounce2"></div> </div> `,})🚀 main.ts: точка запуска Angular
Заголовок раздела «🚀 main.ts: точка запуска Angular»Файл main.ts — это то, с чего начинается жизнь твоего Angular приложения:
Классический подход (с NgModule):
// src/main.ts — традиционный способimport { platformBrowserDynamic } from '@angular/platform-browser-dynamic';import { AppModule } from './app/app.module';
platformBrowserDynamic() .bootstrapModule(AppModule) .catch(err => console.error(err));
// platformBrowserDynamic — платформа для браузера// bootstrapModule(AppModule) — загружаем корневой NgModuleСовременный подход (Standalone, Angular 17+):
// src/main.ts — современный способ без NgModuleimport { bootstrapApplication } from '@angular/platform-browser';import { AppComponent } from './app/app.component';import { provideRouter } from '@angular/router';import { provideHttpClient } from '@angular/common/http';import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), provideHttpClient(), // другие провайдеры... ]}).catch(err => console.error(err));Что происходит при запуске:
1. Браузер загружает index.html2. index.html загружает main.js (скомпилированный main.ts)3. Angular ищет <app-root> в index.html4. Создаёт экземпляр AppComponent5. Рендерит шаблон AppComponent вместо <app-root>6. Начинается жизнь приложения!📦 @NgModule: организация Angular приложения
Заголовок раздела «📦 @NgModule: организация Angular приложения»NgModule — это контейнер, который объединяет компоненты, директивы, пайпы и сервисы в логические блоки:
import { NgModule } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { HttpClientModule } from '@angular/common/http';import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';import { AppComponent } from './app.component';import { HeaderComponent } from './header/header.component';import { FooterComponent } from './footer/footer.component';
@NgModule({ // ① declarations — компоненты, директивы, пайпы этого модуля declarations: [ AppComponent, HeaderComponent, FooterComponent, // Только то, что принадлежит ЭТОМУ модулю // Нельзя объявить компонент в двух модулях! ],
// ② imports — другие модули, чьи exports нам нужны imports: [ BrowserModule, // NgIf, NgFor, AsyncPipe и т.д. HttpClientModule, // HttpClient FormsModule, // ngModel AppRoutingModule, // <router-outlet>, routerLink // UsersModule, // наши фичевые модули ],
// ③ providers — сервисы (лучше использовать providedIn: 'root') providers: [ // { provide: API_URL, useValue: 'https://api.example.com' } ],
// ④ exports — что экспортируем для других модулей exports: [ HeaderComponent, // другие модули смогут использовать <app-header> ],
// ⑤ bootstrap — корневой компонент (только в AppModule!) bootstrap: [AppComponent],})export class AppModule {}Зачем нужен declarations?
Angular должен знать все компоненты заранее. Если забудешь объявить — получишь ошибку:
ERROR: 'app-user-card' is not a known element👋 Hello World: от простого к сложному
Заголовок раздела «👋 Hello World: от простого к сложному»Шаг 1: Базовый Hello World
@Component({ selector: 'app-root', template: `<h1>Привет, мир! 🌍</h1>`})export class AppComponent {}Шаг 2: Добавляем переменную
@Component({ selector: 'app-root', template: `<h1>Привет, {{ name }}! 🌍</h1>`})export class AppComponent { name = 'Яша';}// Результат: "Привет, Яша! 🌍"Шаг 3: Добавляем интерактивность
@Component({ selector: 'app-root', template: ` <h1>Привет, {{ name }}! 🌍</h1> <input [(ngModel)]="name" placeholder="Введи имя"> <p>Ты написал {{ name.length }} символов</p> `})export class AppComponent { name = 'Яша'; // name автоматически обновляется при вводе!}Шаг 4: Список и условия
@Component({ selector: 'app-root', template: ` <h1>Список задач</h1>
<input [(ngModel)]="newTask" (keyup.enter)="addTask()" placeholder="Новая задача"> <button (click)="addTask()">Добавить</button>
<p *ngIf="tasks.length === 0">Задач нет! 🎉</p>
<ul> <li *ngFor="let task of tasks; let i = index"> {{ i + 1 }}. {{ task }} <button (click)="removeTask(i)">✕</button> </li> </ul> `})export class AppComponent { newTask = ''; tasks: string[] = ['Изучить Angular', 'Написать компонент'];
addTask() { if (this.newTask.trim()) { this.tasks.push(this.newTask.trim()); this.newTask = ''; } }
removeTask(index: number) { this.tasks.splice(index, 1); }}🆕 Standalone компоненты (Angular 14+)
Заголовок раздела «🆕 Standalone компоненты (Angular 14+)»Современный Angular движется к миру без NgModule. Standalone компоненты управляют своими зависимостями сами:
// Standalone компонент — не нужен NgModule!@Component({ selector: 'app-user-avatar', standalone: true, // ← ключевое поле! imports: [CommonModule, RouterModule], // ← зависимости прямо здесь template: ` <div class="avatar" [routerLink]="['/users', user.id]"> <img [src]="user.avatarUrl" [alt]="user.name"> <span *ngIf="user.isOnline" class="online-dot"></span> </div> `})export class UserAvatarComponent { @Input() user!: User;}Сравнение подходов:
// ❌ NgModule подход (устаревает)@NgModule({ declarations: [UserAvatarComponent], imports: [CommonModule, RouterModule], exports: [UserAvatarComponent], // чтобы другие могли использовать})export class UsersSharedModule {}
// ✅ Standalone подход (рекомендуется в Angular 17+)@Component({ selector: 'app-user-avatar', standalone: true, imports: [CommonModule, RouterModule], // ... сам себе модуль!})export class UserAvatarComponent {}
// Использование standalone в другом standalone:@Component({ standalone: true, imports: [UserAvatarComponent], // просто импортируем класс компонента template: `<app-user-avatar [user]="currentUser" />`})export class ProfilePageComponent {}Практика
Заголовок раздела «Практика»Попробуйте концепцию в интерактивном редакторе: