Архитектура фронтенд-приложения
Архитектура фронтенд-приложения — набор принципов и паттернов организации кода (разделение ответственности, управление состоянием, структура модулей), определяющих как приложение масштабируется и поддерживается.
Зачем нужно
- Без явной архитектуры код быстро превращается в «спагетти» — компоненты знают друг о друге, бизнес-логика размазана по UI
- Правильная структура позволяет команде работать параллельно, не конфликтуя в одних и тех же файлах
- Архитектурные решения напрямую влияют на тестируемость, производительность и скорость разработки фич
Где используется
- SPA на React, Vue, Angular — выбор между Feature-Sliced Design, слоистой архитектурой, модульной
- Большие монолиты — разбивка на домены/features
- Микрофронтенды — независимые команды с отдельными репозиториями и деплоем
- Next.js / Nuxt — серверный рендеринг требует учёта server/client разделения
Основной контент
Эволюция архитектур: MVC → Flux → современность
MVC (традиционный):
Model ←→ Controller ←→ View
Проблема: двунаправленный поток данных, сложно отследить
Flux (Facebook, 2014):
Action → Dispatcher → Store → View → Action
Однонаправленный поток — предсказуемость
Redux (2015, Flux-производный):
Action → Reducer → Store → React Component → Action
Иммутабельное состояние, чистые функции
Современный React (2022+):
Server Components + Client Components + RSC
Состояние: Zustand / Jotai / TanStack Query
Слоистая архитектура фронтенда
┌─────────────────────────────┐
│ UI Layer (компоненты) │ — рендеринг, пользовательские события
├─────────────────────────────┤
│ Feature Layer │ — бизнес-логика фичи, use cases
├─────────────────────────────┤
│ Domain Layer │ — модели данных, типы, валидация
├─────────────────────────────┤
│ Infrastructure Layer │ — HTTP-клиент, localStorage, WebSocket
└─────────────────────────────┘
Feature-Sliced Design (FSD) — структура папок
src/
app/ — инициализация, провайдеры, роутер
pages/ — страницы (композиция фич)
widgets/ — самостоятельные блоки UI
features/ — пользовательские действия (login, add-to-cart)
entities/ — бизнес-сущности (user, product, order)
shared/ — переиспользуемый код без бизнес-логики
ui/ — базовые компоненты (Button, Input)
api/ — HTTP-клиент
lib/ — утилиты
config/ — константы, env
Управление состоянием
Локальное состояние (useState, useReducer):
Данные живут в одном компоненте или поддереве
Серверное состояние (TanStack Query, SWR):
Кэш ответов API, синхронизация с сервером, loading/error
Глобальное UI-состояние (Zustand, Jotai):
Тема, модальные окна, флаги — без серверных данных
Сложная бизнес-логика (Redux Toolkit):
Строгий unidirectional flow, DevTools, time-travel debugging
Пример разделения ответственности на TypeScript
// entities/user/model.ts — доменная модель
export interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
// shared/api/http-client.ts — инфраструктура
export const httpClient = {
async get<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
},
};
// features/auth/api.ts — feature API
import { httpClient } from "@/shared/api/http-client";
import type { User } from "@/entities/user";
export async function fetchCurrentUser: Promise<User> {
return httpClient.get<User>("/api/me");
}
// features/auth/model.ts — feature state
import { create } from "zustand";
interface AuthState {
user: User | null;
setUser(user: User | null): void;
}
export const useAuthStore = create<AuthState>(set => ({
user: null,
setUser: user => set({ user }),
}));
Принципы чистой архитектуры на фронтенде
- Dependency Rule — зависимости направлены только внутрь (UI → Feature → Domain, но не наоборот)
- Single Responsibility — каждый модуль отвечает за одно
- Interface Segregation — компоненты зависят от минимального интерфейса (пропсы — только то, что нужно)
- Инверсия зависимостей — бизнес-логика не импортирует конкретный HTTP-клиент напрямую
Частые ошибки
- Вся логика в компонентах — fetch, обработка, форматирование — всё в одном компоненте; невозможно тестировать и переиспользовать
- Глобальный стор для всего — серверные данные в Redux вместо TanStack Query приводят к сложной синхронизации
- Circular dependencies между слоями — Feature импортирует из другой Feature напрямую; решение — вынести общее в Shared
- Преждевременная архитектура — FSD для проекта на 3 страницы излишне; начинать просто и рефакторить по необходимости
- Отсутствие явных границ модулей — импорты из глубины чужих модулей (
../../features/auth/internal/store) нарушают инкапсуляцию