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

62. Лучшие практики

Привет! Яша здесь. Последний урок курса! Сегодня соберём всё лучшее, что мы знаем об Angular, в один чеклист. Это production-ready паттерны от реальных проектов 🚀


// ✅ Smart (Container) компонент — знает о стейте
@Component({
selector: 'app-users-page',
template: `
<app-users-list
[users]="users$ | async"
[loading]="loading$ | async"
(deleteUser)="onDelete($event)"
(selectUser)="onSelect($event)">
</app-users-list>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UsersPageComponent {
users$ = this.usersFacade.users$;
loading$ = this.usersFacade.loading$;
constructor(private usersFacade: UsersFacade) {}
onDelete(id: string): void { this.usersFacade.deleteUser(id); }
onSelect(id: string): void { this.usersFacade.selectUser(id); }
}
// ✅ Dumb (Presentational) компонент — только отображение
@Component({
selector: 'app-users-list',
template: `
@if (loading) { <app-spinner /> }
@for (user of users; track user.id) {
<app-user-card
[user]="user"
(delete)="deleteUser.emit(user.id)"
(select)="selectUser.emit(user.id)">
</app-user-card>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush, // ← ВСЕГДА!
})
export class UsersListComponent {
@Input() users: User[] | null = null;
@Input() loading: boolean | null = false;
@Output() deleteUser = new EventEmitter<string>();
@Output() selectUser = new EventEmitter<string>();
}

Файлы:
users.component.ts ✅ feature-name.type.ts
users.component.html
users.component.scss
users.component.spec.ts
users.service.ts
users.module.ts (если не standalone)
users.model.ts или user.model.ts
users.facade.ts
create-user.component.ts ✅ kebab-case для составных
Классы:
UsersComponent ✅ PascalCase + тип
UsersService
UsersModule
UsersFacade
FeatureAuthModule
CreateUserDTO
Переменные:
users$ ✅ $ для Observable
isLoading ✅ is/has для boolean
currentUser ✅ camelCase
MAX_RETRIES = 3 ✅ UPPER_SNAKE для констант

src/
├── app/
│ ├── core/ ← singleton сервисы, guards, interceptors
│ │ ├── auth/
│ │ │ ├── auth.service.ts
│ │ │ ├── auth.guard.ts
│ │ │ └── auth.interceptor.ts
│ │ └── core.module.ts ← импортируется ОДИН раз в AppModule
│ │
│ ├── shared/ ← переиспользуемые компоненты/pipes/directives
│ │ ├── components/
│ │ │ ├── button/
│ │ │ └── modal/
│ │ ├── pipes/
│ │ ├── directives/
│ │ └── index.ts ← barrel export!
│ │
│ ├── features/ ← lazy-loaded feature модули
│ │ ├── users/
│ │ │ ├── components/
│ │ │ ├── pages/
│ │ │ ├── services/
│ │ │ ├── store/
│ │ │ ├── users.routes.ts
│ │ │ └── index.ts
│ │ └── products/
│ │
│ └── app.component.ts

// shared/index.ts — единая точка импорта
export { ButtonComponent } from './components/button/button.component';
export { InputComponent } from './components/input/input.component';
export { ModalComponent } from './components/modal/modal.component';
export { LoadingSpinnerComponent } from './components/loading-spinner/loading-spinner.component';
export { TruncatePipe } from './pipes/truncate.pipe';
export { ClickOutsideDirective } from './directives/click-outside.directive';
// Использование:
// import { ButtonComponent, ModalComponent } from '@/shared';
// Вместо:
// import { ButtonComponent } from '../../../shared/components/button/button.component';
// import { ModalComponent } from '../../../shared/components/modal/modal.component';

// S — Single Responsibility
// ✅ Сервис делает одно дело
@Injectable({ providedIn: 'root' })
export class UserApiService {
getUsers() { return this.http.get<User[]>('/api/users'); }
// ТОЛЬКО HTTP. Не смешивай с форматированием, кэшем, логикой.
}
// O — Open/Closed
// ✅ Расширяй без изменения через абстракции
abstract class Validator {
abstract validate(value: unknown): ValidationErrors | null;
}
// L — Liskov Substitution
// ✅ Подкласс не нарушает контракт базового класса
// I — Interface Segregation
// ✅ Небольшие интерфейсы
interface Readable { read(): Observable<unknown>; }
interface Writable { write(data: unknown): Observable<void>; }
// Не делай один огромный interface Repository
// D — Dependency Inversion
// ✅ Зависи от абстракций, не от конкретных классов
abstract class NotificationService {
abstract notify(message: string): void;
}
@Injectable()
class EmailNotificationService extends NotificationService {
notify(message: string): void { /* email */ }
}
@Component({
providers: [
{ provide: NotificationService, useClass: EmailNotificationService }
]
})
export class OrderComponent {
constructor(private notifications: NotificationService) {} // ← абстракция!
}

// ✅ Change Detection
// - OnPush ВЕЗДЕ
// - async pipe вместо ручного subscribe
// - trackBy во всех *ngFor / @for
// - Иммутабельные данные
// ✅ Bundle size
// - Lazy loading feature modules
// - @defer для off-screen компонентов
// - NgOptimizedImage для изображений
// - Удали неиспользуемые зависимости
// ✅ Memory leaks
// - takeUntilDestroyed() или takeUntil(destroy$)
// - ngOnDestroy для отписки
// - Проверь: componentRef.destroy()
// ✅ Типичные анти-паттерны
// ❌ subscribe() без unsubscribe
// ❌ Методы в шаблонах (вместо pure pipes)
// ❌ ChangeDetectionStrategy.Default
// ❌ * import { everything } from 'lodash'
// ❌ Огромные компоненты (>300 строк — признак проблемы)

// Angular автоматически экранирует привязку данных
// Но есть опасные паттерны:
// ❌ innerHTML без sanitization
@Component({
template: `<div [innerHTML]="userInput"></div>` // Опасно если userInput не очищен!
})
// ✅ Используй DomSanitizer
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
template: `<div [innerHTML]="safeContent"></div>`
})
export class ContentComponent {
safeContent: SafeHtml;
constructor(private sanitizer: DomSanitizer) {
const trustedContent = '<b>Безопасный HTML</b>';
// Используй только для ДОВЕРЕННОГО контента (из своего CMS)
this.safeContent = this.sanitizer.bypassSecurityTrustHtml(trustedContent);
}
}
// ✅ CSP заголовки (nginx.conf)
// add_header Content-Security-Policy "default-src 'self'; script-src 'self';";
// ✅ HTTP Only cookies для JWT (не localStorage!)
// localStorage → виден JS → уязвим к XSS
// HTTP Only cookie → недоступен JS → безопаснее
// ✅ CSRF защита
// HttpClient автоматически отправляет XSRF-TOKEN заголовок
// if (cookie contains XSRF-TOKEN)

// tsconfig.json — максимальная строгость
{
"compilerOptions": {
"strict": true, // включает все strict флаги
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
}
}
// ✅ Не используй any
function process(data: unknown): string {
if (typeof data === 'string') return data;
if (typeof data === 'number') return data.toString();
return JSON.stringify(data);
}
// ✅ Type guards
function isUser(obj: unknown): obj is User {
return typeof obj === 'object' && obj !== null && 'id' in obj;
}
// ✅ Const assertions
const ROLES = ['admin', 'user', 'moderator'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'user' | 'moderator'

Архитектура:
☑ Feature modules с lazy loading
☑ Core/Shared/Features структура
☑ Facade pattern для стейта
☑ Smart/Dumb компоненты
☑ Barrel exports (index.ts)
Производительность:
☑ OnPush везде
☑ trackBy в ngFor
☑ async pipe
☑ @defer для heavy компонентов
☑ NgOptimizedImage
Безопасность:
☑ Нет localStorage для JWT
☑ DomSanitizer для HTML контента
☑ CSP заголовки
☑ HTTP interceptor для auth
Качество кода:
☑ strict TypeScript
☑ ESLint + Prettier
☑ Unit тесты (80%+ coverage)
☑ E2E тесты для критических путей
DevOps:
☑ Budgets в angular.json
☑ CI/CD pipeline
☑ Production builds с source maps (hidden)
☑ nx affected для монорепо

Визуализатор кода: антипаттерны vs лучшие практики: