Перейти к содержимому

3. Первое приложение

Ты создал проект командой ng new, сервер запущен, браузер открыт — и перед тобой стандартная страница Angular. Но что именно происходит? Как Angular запускается? Кто рендерит этот HTML? Давай разберём всё по косточкам! 🦴


Прежде чем разбирать компоненты — нужно понять декораторы. Это 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(...) // Ссылка на дочерний компонент/элемент

Компонент — это сердце Angular. Каждый UI элемент — это компонент. Кнопка, форма, страница, шапка, подвал — всё компоненты.

src/app/users/user-card/user-card.component.ts
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);
}
}

Шаблон компонента:

user-card.component.html
<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 определяет как 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).


// ✅ Отдельный файл — для сложных шаблонов (рекомендуется)
@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 приложения:

Классический подход (с 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 — современный способ без NgModule
import { 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.html
2. index.html загружает main.js (скомпилированный main.ts)
3. Angular ищет <app-root> в index.html
4. Создаёт экземпляр AppComponent
5. Рендерит шаблон AppComponent вместо <app-root>
6. Начинается жизнь приложения!

NgModule — это контейнер, который объединяет компоненты, директивы, пайпы и сервисы в логические блоки:

src/app/app.module.ts
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

Шаг 1: Базовый Hello World

app.component.ts
@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);
}
}

Современный 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 {}

Попробуйте концепцию в интерактивном редакторе: