BMFP — Base Multi Front Platform
Архитектура фронтенда: сайта, веб-приложения, UI десктоп-приложения, мини-аппа. Одна и та же раскладка слоёв работает для любого UI независимо от того, есть ли за ним бэкенд.
12 минутАрхитектура фронтенда: сайта, веб-приложения, UI десктоп-приложения, мини-аппа. Одна и та же раскладка слоёв работает для любого UI независимо от того, есть ли за ним бэкенд.
Слои
boundary → domain → infrastructure → shared| Слой | Что внутри | Может импортировать |
|---|---|---|
arch | роутинг, инициализация, точка входа | все слои (только для bootstrap) |
boundary | страницы, компоненты, виджеты, лэйауты, UI-хуки | domain, infrastructure, shared |
domain | services, states, dtos, enums | infrastructure, 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/
├── <манифест-зависимостей>
└── READMEarch модулю не нужен, пока он не запускается самостоятельно: монтирование и маршруты принадлежат
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/ |
| Класс-синглтон | класс + экспорт единственного экземпляра: BoardServiceClass → BoardService |
| DTO | XxxSchema + выводимый тип 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
поставщика: смена поставщика не задевает прикладной код.
Инварианты
Проверяются поиском по импортам:
boundaryне вызывает транспорт: нет сетевых вызовов и нет импортов клиентов из@infrastructure/clients.- Хранилища — только через хуки из
boundary/_hooks/. sharedне зависит ни от@boundary, ни от@domain, ни от@infrastructure.infrastructureзнает оdomainтолько формы данных: импорт@domain/dtosдопустим, импорт@domain/servicesили@boundary— нет.- Бизнес-логика — в
domain, не в клиентах и не в хранилищах. - Любой ответ сервера проходит через схему валидации в клиенте; сырые данные в хранилище не попадают.
- DTO в
domain/dtos— единственный источник истины о форме данных.
Добавление фичи
- Описать DTO в
domain/dtos/<feature>— форму данных из BMBP. - Завести клиент в
infrastructure/clients/http/<feature>.client; методы валидируют ответ конвертом. - При необходимости — хранилище в
infrastructure/stores/<feature>; для сложного редактируемого состояния —domain/states/<feature>. - Сервис в
domain/services/<feature>.service— оркестрация клиент + хранилище. - UI в
boundary/pages/<feature>/+ маршрут вarch/routers/; компоненты читают через хук, действия зовут сервис. - Утилиты — в
shared, тесты рядом с кодом.
Наш стек
Стек реализации, на которой эта архитектура обкатана. Не часть архитектуры — ориентир.
Базовый набор (нужен любому фронту):
| Роль | Выбор |
|---|---|
| Язык | TypeScript |
| UI | React |
| Роутинг | react-router-dom |
| Сборка | Vite + plugin-react-swc |
| Стили | Sass (sass-embedded) |
Добавляется по мере надобности:
| Когда нужно | Выбор |
|---|---|
| Реактивное состояние (states, stores) | MobX + mobx-react-lite |
| Запросы к API | axios |
| Валидация ответов API | Zod |
| Формы | react-hook-form + @hookform/resolvers (+ react-imask для масок) |
| Просмотр PDF | react-pdf |
Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.