gitaspen docs

BMFP — Base Multi Front Platform

Архитектура фронтенда: сайта, веб-приложения, UI десктоп-приложения, мини-аппа. Одна и та же раскладка слоёв работает для любого UI независимо от того, есть ли за ним бэкенд.

12 минут

Архитектура фронтенда: сайта, веб-приложения, UI десктоп-приложения, мини-аппа. Одна и та же раскладка слоёв работает для любого UI независимо от того, есть ли за ним бэкенд.

Слои

Код
boundary → domain → infrastructure → shared
СлойЧто внутриМожет импортировать
archроутинг, инициализация, точка входавсе слои (только для bootstrap)
boundaryстраницы, компоненты, виджеты, лэйауты, UI-хукиdomain, infrastructure, shared
domainservices, states, dtos, enumsinfrastructure, shared
infrastructureклиенты протоколов, хранилища (stores)shared
sharedутилиты, конфиги, интеграции, общие типыничего

Правила:

  • boundary не вызывает клиенты напрямую и не дёргает stores — только через хуки _hooks/. За данными ходит в domain-сервисы.
  • domain содержит всю бизнес-логику, не зависит от UI.
  • infrastructure — адаптеры (протоколы, хранение), без бизнес-логики.
  • shared ни от кого не зависит.

Структура каталога

Код
app/
├── arch/                        # точка входа: роутинг + инициализация
│   ├── entry                    # bootstrap: монтирует приложение в роутер
│   ├── mfe-entry                # точка монтирования для микрофронтенда
│   ├── routers/                 # маршруты, guards
│   └── boot/                    # сплэш, предзагрузка
├── boundary/
│   ├── pages/<feature>/
│   ├── components/
│   ├── widgets/
│   ├── layouts/
│   ├── _hooks/                  # UI-хуки: подписка на хранилища
│   └── __styles/
├── domain/
│   ├── services/<domain>        # оркестраторы: клиент + хранилище
│   ├── states/<domain>          # доменные состояния (формы, корзины, расчёты)
│   ├── dtos/<domain>            # схемы валидации данных
│   └── enums/
├── infrastructure/
│   ├── clients/
│   │   ├── http/                # базовый клиент + клиенты на API + хранение токена
│   │   └── ws, sse, …           # прочие протоколы по мере надобности
│   └── stores/<domain>          # хранилища + базовое хранилище
└── shared/
    ├── utils/                   # ошибки, форматирование, общие хуки
    ├── configs/                 # URL, константы
    ├── integrations/            # обёртки внешних SDK/платформ
    └── types/

Алиасы импортов @arch, @boundary, @domain, @infrastructure, @shared в конфиге сборки и типов закрепляют границы слоёв.

Пакет фронта целиком, поверх app/:

Код
<front>/
├── app/                # код по слоям выше
├── modules/            # source-first frontend-модули (опц.; отдельные репо/пакеты)
├── public/             # статика
├── <манифест-зависимостей>   # зависимости + скрипты dev/build
├── <конфиг-сборки>     # бандлер + алиасы слоёв
├── <конфиг-типов>
├── container           # образ
└── .secrets/           # переменные окружения (вне VCS)

Frontend-модули

modules/ — место для крупных переиспользуемых UI-способностей: консоли разработчика, редактора доски, markdown-движка, просмотрщика файлов. Это не свалка общих компонентов и не копия кода потребителя. Каждый модуль — самостоятельная source-first единица со своим репозиторием, манифестом, тестами и публичным контрактом:

Код
modules/
└── <module>/
    ├── app/
    │   ├── boundary/           # собственный UI модуля
    │   ├── domain/             # состояния и сценарии модуля
    │   ├── infrastructure/     # stores и стандартные адаптеры
    │   └── shared/             # внутренний фундамент модуля
    ├── public                  # единственная публичная точка импорта
    ├── tests/
    ├── <манифест-зависимостей>
    └── README

arch модулю не нужен, пока он не запускается самостоятельно: монтирование и маршруты принадлежат host-приложению. Если модуль получает собственный mfe-entry, отдельный бандл и независимый жизненный цикл поставки — это уже микрофронтенд из frontend/microfrontends/, а не встроенный модуль.

Модуль подключается в host одной композицией через public: получает порты/адаптеры, начальные данные, тему и слоты; наружу отдаёт UI, команды и события. Он не импортирует код host-приложения и не содержит ветвлений по имени потребителя. Кастомизация делается значениями контракта, а не форком:

Код
host (arch/boot или boundary)
  → module.public({ transport, permissions, theme, slots })
    → внутренние boundary → domain → infrastructure → shared

Исходники модуля физически присутствуют в локальном workspace и входят в сборку фронта. Его репозиторий подключается менеджером пакетов, workspace или git-submodule с закреплённой версией. После первоначального checkout разработка и сборка работают офлайн. Универсальное исправление коммитится в репозиторий модуля; продукт затем обновляет закреплённую версию. Постоянные ветки и копии под каждого потребителя запрещены.

Мелкие компоненты, нужные только текущему фронту, остаются в app/boundary/components. Способность переезжает в modules/ по правилу REUSE: когда появился второй потребитель либо собственный независимый жизненный цикл.

Точка входа

В arch/entry приложение монтируется в роутер:

Код
root = найти корневой узел разметки
смонтировать(root, Router(Lazy(AppRoot)))

AppRoot — boot + маршруты из arch/routers/. Для микрофронтенда — отдельная точка arch/mfe-entry и отдельная сборка. Выбор платформы и аутентификация — в boot или domain-сервисе.

Что в каждом слое

boundary

Страницы, компоненты, виджеты, лэйауты, UI-хуки. Не делает сетевых вызовов и не знает форму ответа сервера. За операциями ходит в domain-сервисы. За состоянием — в хранилища, но только через хуки _hooks/, которые подписываются на снимок состояния.

domain

  • services — оркестраторы. Один сервис = одна доменная зона. Вызывают клиент из infrastructure, валидируют, кладут результат в хранилище. Не трогают сетевой примитив, зовут клиент.
  • states — доменные состояния. Сложные объекты с бизнес-логикой (форма заказа, корзина, редактирование): считают, валидируют, освобождают ресурсы.
  • dtos — схемы валидации данных. Источник истины о форме данных, типы выводятся из схем.
  • enums — доменные перечисления (статусы, типы), не UI-подписи.

infrastructure

  • clients — клиенты протоколов. Базовый клиент: подстановка токена, реакция на «разлогинило», разбор конверта, валидация ответа. От него — клиенты конкретных API. Не содержат бизнес-логики.
  • stores — хранилища. Отвечают на «где и как хранить» (память / локальное хранилище / offline). Отдают компонентам реактивный снимок, мутации атомарны. Только операции хранения.

shared

Утилиты, конфиги, интеграции, общие типы. Только то, что нужно нескольким слоям.

States и Stores

states (domain/states/)stores (infrastructure/stores/)
отвечает начто такое состояние и его логикагде и как хранить данные
содержитбизнес-логику (расчёты, валидация, автопересчёт)стратегию хранения
примерысостояние формы заказахранилище истории с сервера; хранилище экземпляров состояний

Store управляет жизненным циклом экземпляров State (создаёт, отдаёт, освобождает). Простые данные с сервера лежат в data-хранилище напрямую; сложное редактируемое состояние — это State, который держится в Store.

Контракт с бэкендом

Все ответы бэкенда приходят в конверте:

Код
успех:  { status, data, details? }
ошибка: { status, error_code, details }

error_code — машиночитаемый код (по нему фронт ветвится), details — текст для человека. Конверт описан схемой валидации в infrastructure/clients/http/.

Поток данных: компонент через хук вызывает domain-сервис → сервис зовёт клиент → клиент бьёт в бэк, разворачивает конверт, валидирует по схеме → сервис кладёт DTO в хранилище → компонент перерисовывается.

Базовый клиент

Над сетевым вызовом стоит один базовый клиент, который снимает с прикладного кода:

  • подстановку текущего токена в каждый запрос;
  • реакцию на отказ авторизации (чистит сессию, уводит на вход);
  • разворачивание конверта — наружу отдаётся только содержимое;
  • валидацию ответа по схеме (при расхождении — явный сбой, не «тихо неправильные» данные).

Добавить вызов к новому эндпойнту = описать один метод с адресом и схемой.

Ошибки транспорта проходят через один обработчик и приходят в виде типовой ошибки с error_code; UI показывает их единообразно.

Микрофронтенды

Микрофронтенд — самостоятельный BMFP-фронт, монтируемый в оболочку (shell). Экспортирует точку монтирования mount(el, props) в условленный глобальный ключ; собирается отдельным бандлом. Контракт оболочки (доступ к auth и навигации) описан у shell, MFE общается с ним только через props. Внутри MFE — те же четыре слоя, свой роутер и свои клиенты.

Мультиплатформенность

Платформа выбирается на старте по среде запуска (обычный web с формой входа, встроенный мини-апп с авто-входом по данным платформы, десктоп-оболочка). Слои не меняются — меняется только способ аутентификации в domain-сервисе.

Именование

ЧтоПравило
Файлы по ролисуффикс роли: board.dto, board.service, board.client, board.store, block.enums
Служебное / приватноепрефикс _ / __: _hooks/, _base.store, __styles/
Класс-синглтонкласс + экспорт единственного экземпляра: BoardServiceClassBoardService
DTOXxxSchema + выводимый тип Xxx; списки XxxListSchema
ХукuseXxx
Импортыалиасы слоёв, всегда абсолютные

Маршруты и защита

Маршруты живут в arch/routers/. Страница подключается лениво — так стартовый бандл содержит оболочку и текущий экран, а не все страницы приложения сразу:

Код
MapPage = lazy(() => import('@boundary/pages/map/map'))

<Route path="/map" element={<Protected><MapPage/></Protected>} />

Protected — обёртка из arch/routers/: спрашивает авторизацию у domain-сервиса и уводит на вход, если её нет. Проверка прав живёт в одном месте, а не в каждой странице.

Стили

Стиль лежит рядом с тем, что он оформляет: component.tsx + component.module.scss. Локальные имена классов исключают протекание стилей между компонентами.

Глобально — только то, что не принадлежит компоненту: сброс, шрифты, токены темы (цвета, отступы, радиусы, типографика) в boundary/__styles/. Компоненты используют токены, а не собственные значения цветов и отступов: тема меняется в одном месте.

Конфигурация и окружение

Значения окружения хранятся вне репозитория (.secrets/) и попадают в сборку через конфиг бандлера. Переменные, попадающие в клиентский бандл, помечаются оговорённым префиксом — он делает явным, что значение станет публичным.

Секрет в клиентском бандле не защищён ничем: код фронта доступен пользователю целиком. Всё, что должно оставаться тайной, живёт на бэкенде.

Адреса API и константы — в shared/configs/, а не в коде компонентов.

Оптимизация загрузки

Что даёт основной выигрыш при холодном входе:

  • разделение по маршрутам — страницы через lazy(); иначе стартовый бандл содержит и те экраны, которые пользователь не открывал;
  • отдельный чанк для стабильных зависимостей (фреймворк, роутер) — он не меняется между релизами и остаётся в кэше браузера;
  • ленивая загрузка тяжёлых библиотек (просмотр PDF, редакторы, разбор разметки) — только на экране, где они нужны;
  • кэширование статики: файлы с хэшем в имени — на год как неизменяемые, точка входа — без кэша, иначе релиз не доедет до пользователя.

Ленивая загрузка добавляет режим отказа: после релиза имена файлов меняются, и открытая вкладка может запросить исчезнувший чанк. Обработчик такой ошибки перезагружает страницу один раз — иначе пользователь видит пустой экран.

Ошибки и аналитика

Ошибки транспорта приводятся к одному типу в базовом клиенте (см. выше) и обрабатываются единообразно. Прикладной код не разбирает исключения на месте вызова, а получает результат в предсказуемой форме — успех либо типовая ошибка с error_code.

Аналитика и логирование — обёртки в shared/utils/. Компоненты вызывают обёртку, а не SDK поставщика: смена поставщика не задевает прикладной код.

Инварианты

Проверяются поиском по импортам:

  1. boundary не вызывает транспорт: нет сетевых вызовов и нет импортов клиентов из @infrastructure/clients.
  2. Хранилища — только через хуки из boundary/_hooks/.
  3. shared не зависит ни от @boundary, ни от @domain, ни от @infrastructure.
  4. infrastructure знает о domain только формы данных: импорт @domain/dtos допустим, импорт @domain/services или @boundary — нет.
  5. Бизнес-логика — в domain, не в клиентах и не в хранилищах.
  6. Любой ответ сервера проходит через схему валидации в клиенте; сырые данные в хранилище не попадают.
  7. DTO в domain/dtos — единственный источник истины о форме данных.

Добавление фичи

  1. Описать DTO в domain/dtos/<feature> — форму данных из BMBP.
  2. Завести клиент в infrastructure/clients/http/<feature>.client; методы валидируют ответ конвертом.
  3. При необходимости — хранилище в infrastructure/stores/<feature>; для сложного редактируемого состояния — domain/states/<feature>.
  4. Сервис в domain/services/<feature>.service — оркестрация клиент + хранилище.
  5. UI в boundary/pages/<feature>/ + маршрут в arch/routers/; компоненты читают через хук, действия зовут сервис.
  6. Утилиты — в shared, тесты рядом с кодом.

Наш стек

Стек реализации, на которой эта архитектура обкатана. Не часть архитектуры — ориентир.

Базовый набор (нужен любому фронту):

РольВыбор
ЯзыкTypeScript
UIReact
Роутингreact-router-dom
СборкаVite + plugin-react-swc
СтилиSass (sass-embedded)

Добавляется по мере надобности:

Когда нужноВыбор
Реактивное состояние (states, stores)MobX + mobx-react-lite
Запросы к APIaxios
Валидация ответов APIZod
Формыreact-hook-form + @hookform/resolvers (+ react-imask для масок)
Просмотр PDFreact-pdf
Инструкция не помогла?

Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.