Архитектура фронтенда — 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)
Слои (сверху вниз): 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) |
| Repository | API-клиент / WS-клиент (adapter) |
| Infrastructure | infrastructure (fetch, WebSocket, localStorage) |
UseCase на фронте — это функция/действие: валидирует вход, применяет правила (guard), делает побочные эффекты (сеть, WS), обновляет стор, переводит ошибки в понятные сообщения. Компонент остаётся «глупым» — только UI.
Guard-паттерн в UseCase
tsexport 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 в features | UI работает с готовыми данными, чистые компоненты | Дополнительный слой маппинга |
🧩 Проверь себя
🔗 Мостик к Модулю 3
Архитектура спроектирована. Настроим инфраструктуру: API и WebSocket клиенты.