Архитектура фронтенда — FSD + Clean Architecture

Из M1: мы настроили проект (Vite + React + TS). Теперь задаём архитектуру, чтобы код было легко масштабировать и тестировать.

Перед началом: Модуль 1. Инструменты: FSD (Feature-Sliced Design). Время: ~5 ч.


🔥 Инцидент. Фронтенд растёт: 50 компонентов в папке components/. Imports хаотичные: ../../../../../utils. При изменении одного API — ломается половина приложения. Нужна архитектура: FSD — слои (app → pages → widgets → features → entities → shared), каждый слой зависит только от нижних.


📖 Теория

Фокус: архитектура — это UX разработчика. Чёткие границы, явные зависимости, тестируемость. Стек: React + TypeScript + Vite + FSD.

Feature-Sliced Design (FSD)

Слои FSD и направление зависимостей

Слои (сверху вниз): app → widgets → features → entities → shared. Правило: импорты идут только вниз. Вышележащий слой зависит от нижележащего, но не наоборот.

  • app — точка входа, роутинг, провайдеры, composition root (DI).
  • widgets — композитные блоки (ChatList, ChatRoom).
  • features — бизнес-фичи (sendMessage, searchChats, useAuth) — здесь живут UseCase и guards.
  • entities — доменные типы (Chat, Message, User).
  • shared — UI-kit, хуки (useDebounce), утилиты.

Каждый слой/слайс выставляет публичный API (index.ts) — остальные зависят только от экспортированного.

Clean Architecture на фронте

Те же идеи, что на бэкенде, но названия адаптированы:

БэкендФронтенд
Domain (entity)entities (типы: Message, Chat, User)
Application (UseCase)features (action / command handler с guards)
RepositoryAPI-клиент / WS-клиент (adapter)
Infrastructureinfrastructure (fetch, WebSocket, localStorage)

UseCase на фронте — это функция/действие: валидирует вход, применяет правила (guard), делает побочные эффекты (сеть, WS), обновляет стор, переводит ошибки в понятные сообщения. Компонент остаётся «глупым» — только UI.

Guard-паттерн в UseCase

ts
export const sendMessage = async (roomId: string, text: string) => {
  if (!roomId) throw new Error("roomId is required")
  const trimmed = text.trim()
  if (!trimmed) throw new Error("Сообщение не может быть пустым")
  if (trimmed.length > 4000) throw new Error("Сообщение слишком длинное")
  // ... основная логика без вложенности
}

Все краевые случаи — в начале функции (guard clauses), основная логика остаётся линейной.

ViewModel

Во features удобно держать ViewModel: она берёт данные из entities, преобразует в формат для UI и инкапсулирует логику взаимодействия (валидация перед отправкой). Компоненты widgets работают с ViewModel, а не с сырыми данными.

Контроль зависимостей

Правила FSD имеет смысл подкрепить линтером (например, eslint-plugin-import с ограничением путей или кастомным скриптом), запрещающим импорты «чужих» слоёв. Это не даёт архитектуре постепенно превратиться в спагетти при росте команды.

Практика

  • Описать структуру папок под чат (features/chats, features/messages, features/autocomplete, features/auth).
  • Написать правило/скрипт, запрещающий импорт из нижележащего слоя в вышележащий (например, entities → features).

💱 Компромиссы (Trade-offs)

РешениеПлюсыМинусы
FSD (app→widgets→features→entities→shared)Чёткие зависимости, тестируемость, масштабированиеПорог входа, больше папок/файлов
Clean Architecture на фронтеUseCase/guards переиспользуемы, «глупые» компонентыЛишний код для простых фич
Публичный API слоя (index.ts)Инкапсуляция, контроль зависимостейЛегко забыть экспорт, боль при рефакторинге
eslint-rule для зависимостейАрхитектура не превращается в спагеттиНастройка, false-positives
ViewModel в featuresUI работает с готовыми данными, чистые компонентыДополнительный слой маппинга

🧩 Проверь себя

1. shared слой в FSD — что в него кладут?
2. entities в FSD — что хранят?
3. Почему бизнес-логика в entities/features, а не в UI-компонентах?
4. FSD (Feature-Sliced Design) — главное правило?
5. Composition Root во frontend — где и что?

🔗 Мостик к Модулю 3

Архитектура спроектирована. Настроим инфраструктуру: API и WebSocket клиенты.