Когда над фронтендом крупного e-commerce приложения работают десятки команд, классическая организация проекта по техническим слоям перестает справляться. Общие папки components/, services/ и hooks/ удобны на старте, но в масштабе маркетплейса быстро превращаются в распутанный монолит. Изменение логики скидок в корзине случайно ломает каталог, а сквозные циклические импорты делают рефакторинг рискованным.
Команда фронтенд-инфраструктуры маркетплейса RWB применила архитектурный подход: сохранение единого модульного монолита с декомпозицией кодовой базы на автономные feature-модули, связываемые через легковесный DI-контейнер (Dependency Injection). В отличие от микрофронтендов, этот подход сохраняет единый цикл сборки и общую дизайн-систему, обеспечивая командам четкие границы ответственности.
Пределы технических слоев и выбор между FSD и DI
Традиционная структура группирует файлы по их роли во фреймворке: компоненты лежат в одной папке, запросы к API — во второй, утилиты — в третьей. Разработчик, реализующий оформление заказа (checkout), вынужден постоянно переключаться между разными концами проекта. Контекст предметной области размывается, а связи между экранами становятся неконтролируемыми.
Методология Feature-Sliced Design (FSD) решает эту проблему через жесткую слоистую иерархию (app, pages, widgets, features, entities, shared). Однако в реальных масштабных маркетплейсах строгие рамки FSD иногда порождают бюрократию: соседние доменные сущности требуют перекрестного взаимодействия, а вынесение логики вниз раздувает слой shared.
Подход на основе feature-модулей с DI предлагает гибкий компромисс:
| Архитектурный подход | Порог входа | Изоляция доменов | Масштабируемость команд | Контроль связности |
|---|---|---|---|---|
| Технические слои | Низкий | Слабая | До 5–10 инженеров | Отсутствует |
| Feature-Sliced Design | Средний | Строгая иерархическая | Десятки команд | Правила слоев |
| Feature-модули + DI | Средний | Модульная контрактная | Десятки команд | Интерфейсы и DI |
В центре архитектуры RWB лежит вертикальное разделение приложения на функциональные бизнес-модули: catalog, cart, delivery, checkout. Каждый модуль представляет собой замкнутую подсистему для конкретного пользовательского сценария.
Роль Dependency Injection и точка сборки приложения
Внедрение зависимостей (DI) традиционно считается серверным паттерном, однако в интерфейсе оно выполняет важную функцию: отделяет правила бизнеса от жизненного цикла компонентов React.
Без DI модуль оформления заказа напрямую импортировал бы реализацию доставки: import { DeliveryService } from "@/features/delivery". В этом случае checkout жестко привязывается к конкретному сервису доставки и его сетевым клиентам. Протестировать логику оформления заказа без поднятия всего окружения доставки становится невозможно.
При использовании DI модуль объявляет только интерфейс потребности (consumer-owned contract):
export interface DeliveryCalculator {
calculateCost(items: OrderItem[]): Promise<Money>;
}
export class CheckoutService {
constructor(private deliveryCalc: DeliveryCalculator) {}
}
Конкретный сервис передается в конструктор снаружи. Местом, где интерфейсы соединяются с реализациями, выступает точка сборки — composition root на уровне приложения (app). DI-контейнер автоматизирует регистрацию токенов и фабрик, избавляя инженеров от ручной связки объектов.
Анатомия образцового feature-модуля
Каждый модуль внутри кодовой базы разграничивает роли внутренних слоев:
api/: клиенты сетевых запросов, DTO и функции преобразования данных бэкенда в доменные модели.model/: сущности предметной области, чистые функции расчета, валидаторы и интерфейсы сервисов.services/: классы бизнес-логики, реализующие пользовательские сценарии и получающие зависимости через конструктор.ui/: визуальные компоненты React, отвечающие за отрисовку интерфейса.hooks/: реактивные адаптеры, связывающие вызовы сервисов с состоянием компонентов и пользовательским вводом.index.ts: публичный фасад (Public API) модуля.
Фасад выполняет роль единственного шлюза во внешний мир. Через него экспортируются только интерфейсы сервисов, готовые визуальные виджеты и фабрики. Прямой импорт внутренних файлов модуля в обход фасада запрещен: если внешнему коду требуется внутренняя функция, ее выносят в публичный контракт осознанно.
Управление состоянием и изоляция платформенных API
Распространенная ошибка — попытка сложить все данные приложения в единое глобальное хранилище. В подходе RWB состояние разделено по природе данных:
- Серверное состояние (Server Cache): списки товаров, параметры заказов и каталог кэшируются через специализированные клиенты (React Query или RTK Query).
- Клиентское состояние фичи (Feature State): данные, нужные нескольким компонентам модуля (например, черновик заказа), инкапсулируются внутри модуля через легковесные хранилища (Zustand или Context).
- Локальное состояние UI: статус открытия меню или фокус инпута остаются внутри стандартных хуков
useState.
Не менее строгая граница возводится вокруг браузерных API. Прямое обращение к window, localStorage, cookies или navigator из бизнес-логики запрещено. Доступ изолируется в адаптерах слоя shared/platform. Адаптер обрабатывает ошибки переполнения квоты и позволяет выполнять код на сервере (Server-Side Rendering, SSR) без риска падения из-за отсутствия глобального объекта window.
Контроль границ через ESLint и готовность к ИИ-ассистентам
Архитектурные соглашения в RWB контролируются статическим анализатором ESLint на уровне CI-пайплайна:
- Запрет глубоких импортов: блокируются попытки импорта из внутренних папок чужих модулей.
- Запрет циклических перекрестных зависимостей: модули одного уровня не импортируют исполняемый код друг друга напрямую.
- Изоляция окружения: запрещено прямое обращение к глобальным переменным браузера вне платформенных адаптеров.
Дополнительный эффект модульности — готовность кодовой базы к работе с ИИ-ассистентами (LLM-ready codebase). Изолированные модули с явными контрактами index.ts и записями архитектурных решений (ADR) сокращают контекстное окно для нейросети. Ассистент генерирует код в границах одного модуля, не галлюцинируя скрытыми зависимостями, а линтер мгновенно отсекает некорректные импорты.
План постепенной миграции и критерии готовности
Переход на модульную архитектуру не требует остановки продуктовой разработки:
- Выделение первой изолированной фичи: выбирается модуль с минимальным числом связей (например, избранное или профиль).
- Формирование публичного контракта: создается файл
index.tsс экспортом только необходимых типов и компонентов. - Подключение правила линтера: в CI активируется запрет глубоких импортов для переработанного модуля.
- Вынесение зависимостей в composition root: создание сервисов переносится на уровень приложения, а модуль начинает принимать зависимости через интерфейсы.
- Покрытие тестами: пишутся юнит-тесты на чистую логику сервисов без jsdom и интеграционный тест сборки контейнера зависимостей.
Модульная модель позволяет маркетплейсу развивать сложные сценарии силами распределенных команд, сохраняя предсказуемость кодовой базы и скорость поставки изменений.
