Архитектурные паттерны
Проект использует современные архитектурные паттерны Angular для обеспечения масштабируемости, тестируемости и удобства разработки.
Standalone Components
Angular 20 рекомендует использовать standalone компоненты вместо NgModules.
Что это даёт
- ✅ Меньше boilerplate кода
- ✅ Явные зависимости
- ✅ Лучшая tree-shaking
- ✅ Проще для новичков
- ✅ Лучше поддержка в IDE
Пример
import { Component, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { UserService } from "./services/user.service";
// ✅ Standalone компонент
@Component({
selector: "app-user-list",
standalone: true, // Ключевой флаг
imports: [CommonModule, MatButtonModule],
templateUrl: "./user-list.component.html",
styleUrl: "./user-list.component.scss"
})
export class UserListComponent {
private readonly userService = inject(UserService);
users$ = this.userService.getAll();
}Структура импортов
// 1. Angular core
import { Component, inject } from "@angular/core";
// 2. Angular modules и Common
import { CommonModule } from "@angular/common";
import { ReactiveFormsModule } from "@angular/forms";
// 3. Material модули
import { MatButtonModule } from "@angular/material/button";
import { MatTableModule } from "@angular/material/table";
// 4. Local components и директивы
import { UserCardComponent } from "./components/user-card.component";
// 5. Services
import { UserService } from "./services/user.service";
@Component({
selector: "app-users",
standalone: true,
imports: [CommonModule, ReactiveFormsModule, MatButtonModule, MatTableModule, UserCardComponent],
template: `...`
})
export class UsersComponent {}Dependency Injection через inject()
Функциональный DI API вместо конструктора.
Преимущества
- ✅ Чище и понятнее
- ✅ Ленивая инициализация
- ✅ Проще для тестирования
- ✅ Работает везде, не только в конструкторе
Пример
// ❌ Старый стиль (конструктор)
export class UserComponent {
constructor(
private userService: UserService,
private router: Router,
private logger: LoggerService
) {}
}
// ✅ Новый стиль (inject API)
export class UserComponent {
private readonly userService = inject(UserService);
private readonly router = inject(Router);
private readonly logger = inject(LoggerService);
}
// ✅ Использование в функциях
export function loadUser(id: string) {
const userService = inject(UserService);
return userService.getById(id);
}State Management с @ngneat/elf
Elf — это легковесный, типобезопасный state management для Angular.
Зачем нужен state management?
- ✅ Единый источник истины
- ✅ Предсказуемое управление состоянием
- ✅ Легче отладить
- ✅ Реактивные обновления
Структура Elf store
// stores/users.store.ts
import { createStore } from "@ngneat/elf";
import { withEntities } from "@ngneat/elf-entities";
export interface IUser {
id: string;
name: string;
email: string;
}
// Создаём store
export const usersStore = createStore({ name: "users" }, withEntities<IUser>());
// Выносим actions
export const updateUsersAction = createAction("[Users] Update Users");
// Reducer
updateUsersAction.subscribe(() => {
usersStore.reset();
});Использование в компоненте
@Component({
selector: "app-users-list",
standalone: true,
imports: [CommonModule],
template: `
<div *ngFor="let user of users$ | async">
{{ user.name }}
</div>
`
})
export class UsersListComponent {
private readonly usersStore = inject(usersStore);
// Получить users$ observable
users$ = this.usersStore.pipe(selectAllEntities());
}Reactive Forms
Typed Reactive Forms с полной типизацией.
Преимущества
- ✅ Полная типизация формы
- ✅ Типизированные контролы
- ✅ Лучшая поддержка в IDE
- ✅ Меньше ошибок runtime
Пример
import { Component, inject } from "@angular/core";
import { FormGroup, FormControl, ReactiveFormsModule } from "@angular/forms";
interface IUserForm {
name: FormControl<string | null>;
email: FormControl<string | null>;
age: FormControl<number | null>;
}
@Component({
selector: "app-user-form",
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="name" />
<input formControlName="email" type="email" />
<input formControlName="age" type="number" />
<button type="submit">Submit</button>
</form>
`
})
export class UserFormComponent {
form = new FormGroup<IUserForm>({
name: new FormControl(null),
email: new FormControl(null),
age: new FormControl(null)
});
submit() {
if (this.form.valid) {
const data = this.form.getRawValue();
// data типизирован как IUserForm
}
}
}HTTP & Interceptors
Обработка HTTP с автоматическими перехватчиками.
API Service Layer
// ✅ API Service
@Injectable({ providedIn: "root" })
export class ApiService {
private readonly http = inject(HttpClient);
private readonly baseUrl = environment.apiUrl;
get<T>(endpoint: string) {
return this.http.get<IApiWrapper<T>>(`${this.baseUrl}${endpoint}`).pipe(map((response) => response.data));
}
}
// ✅ Доменный Сервис
@Injectable({ providedIn: "root" })
export class UserService {
private readonly api = inject(ApiService);
getUsers() {
return this.api.get<IUser[]>("/users");
}
}
// ✅ Компонент
export class UsersComponent {
private readonly userService = inject(UserService);
users$ = this.userService.getUsers();
}Interceptors
// ✅ Auth Interceptor
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem("access_token");
if (token) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
}
return next(req);
};
// ✅ Error Interceptor
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Logout и перенаправить
}
return throwError(() => error);
})
);
};
// ✅ Используем в main.ts
bootstrapApplication(AppComponent, {
providers: [provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]))]
});Routing & Lazy Loading
Маршрутизация с ленивой загрузкой для оптимизации.
Структура маршрутов
export const appRoutes: Routes = [
{
path: "auth",
children: [
{
path: "login",
canActivate: [noAuthGuard],
loadComponent: () => import("./pages/login/login.component").then((m) => m.LoginComponent)
}
]
},
{
path: "admin",
canActivate: [authGuard],
loadComponent: () => import("./pages/admin/admin.component").then((m) => m.AdminComponent),
children: [
{
path: "users",
loadComponent: () => import("./pages/users/users.component").then((m) => m.UsersComponent)
}
]
}
];Guards
Защита маршрутов с современным API.
Функциональные Guards
// ✅ Auth Guard
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.isAuthenticated) {
return router.createUrlTree(["/login"]);
}
return true;
};
// ✅ No Auth Guard
export const noAuthGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated) {
return router.createUrlTree(["/dashboard"]);
}
return true;
};
// ✅ Использование
const routes: Routes = [
{
path: "admin",
canActivate: [authGuard],
loadComponent: () => import("./admin.component")
}
];RxJS Subscription Management (@UntilDestroy)
Для автоматической отписки от Observable используем @ngneat/until-destroy.
Базовое использование
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
@UntilDestroy()
@Component({
selector: "app-my-component",
standalone: true,
templateUrl: "./my-component.html",
styleUrl: "./my-component.scss"
})
export class MyComponent {
private readonly service = inject(MyService);
someMethod() {
this.service
.getData()
.pipe(untilDestroyed(this))
.subscribe((data) => {
// Автоматическая отписка при destroy компонента
});
}
}Когда использовать
- ✅ При ручных подписках в методах компонента
- ✅ В обработчиках событий, которые вызывают subscribe()
- ❌ Не нужно с
toSignal()— он сам управляет подпиской - ❌ Не нужно с
| asyncв шаблонах
Пример с диалогами
@UntilDestroy()
@Component({
/* ... */
})
export class IntegrationsComponent {
private readonly dialogService = inject(DialogService);
private readonly service = inject(IntegrationsService);
createItem() {
this.dialogService
.onSuccess(CreateDialogComponent)
.pipe(
switchMap((data) => this.service.create(data)),
untilDestroyed(this)
)
.subscribe();
}
}Signals & toSignal (Angular 17+)
Signals are a modern reactivity primitive in Angular. Use toSignal() to convert Observables to Signals for better performance and cleaner code.
Why Signals over Observables?
- ✅ Synchronous, easier to understand
- ✅ Better change detection (OnPush friendly)
- ✅ Integrated with Angular's rendering
- ✅ No
| asyncpipe needed
toSignal Pattern (Recommended)
export class MyComponent {
private readonly route = inject(ActivatedRoute);
private readonly service = inject(MyService);
// Extract route parameter
readonly id$ = this.route.paramMap.pipe(
map((p) => p.get("id")),
filter(Boolean)
);
// Convert to signal
readonly id = toSignal(this.id$);
// Load data based on id
readonly data = toSignal(this.id$.pipe(switchMap((id) => this.service.getById(id))), { initialValue: [] });
}In Templates
Use signals directly without async pipe:
<!-- Before -->
<div>{{ (data$ | async)?.name }}</div>
<!-- After -->
<div>{{ data()?.name }}</div>Usage Guidelines
- Always provide
initialValuefortoSignal()to avoid undefined - Use
filter(Boolean)to prevent null/undefined values - Combine with
switchMapfor dependent requests - Add
ChangeDetectionStrategy.OnPushfor performance
Control Flow (Angular 17+)
Современный синтаксис вместо *ngIf, *ngFor.
// Текущий подход
<div *ngIf="user$ | async as user">
<p>{{ user.name }}</p>
<div *ngFor="let item of (items$ | async)">
{{ item }}
</div>
</div>
// Новый подход
@if (user$ | async as user) {
<p>{{ user.name }}</p>
}
@for (item of (items$ | async); track item.id) {
<div>{{ item }}</div>
}
@switch (status$ | async) {
@case ("loading") {
<p>Loading...</p>
}
@case ("error") {
<p>Error!</p>
}
@default {
<p>Done</p>
}
}Best Practices
✅ Делайте:
- Используйте standalone компоненты
- Используйте
inject()API - Ленивая загрузка маршрутов
- Типизируйте всё
- Используйте functional guards и interceptors
- Используйте
@UntilDestroy()+untilDestroyed(this)для подписок
❌ Не делайте:
- Не смешивайте NgModules и standalone
- Не используйте конструкторы для DI
- Не забывайте про
trackв*ngFor(или@for) - Не используйте
anyтипы - Не забывайте отписываться от Observable
Следующие шаги
- Архитектура — общая архитектура
- Правила кода — стандарты
- Качество кода — линтинг и форматирование