Всем привет! Меня зовут Иван Соснович, я сеньор фронтенд-разработчик в СберТехе, тружусь в команде Platform V Kintsugi — это графический инструмент для сопровождения, мониторинга и диагностики Postgres-like СУБД. По итогам прошлой статьи мнения читателей разделились.
В эпоху ИИ о структуре кода задумываются все реже, однако архитектурные решения остаются критически важными для развития и долгосрочной поддержки систем. Надеюсь, что эта статья будет полезна и появятся советы, как упростить и облегчить систему сборки.
Сразу скажу, что код проекта будет писать ИИ через GigaCode CLI, скиллы останутся в репозитории.
Предусловия: почему мы этим занимаемся
В нашу команду поступил запрос: сможем ли мы отдавать часть своих фичей для встраивания в другие сервисы, чтобы они были независимыми, могли работать и поддерживать свою функциональность. По части бэкенда и DevOps — отдельные нюансы. Но основной вопрос возник: как из большого монолита фронтенд-приложения выделить часть логики и отдать только её, без лишнего кода. И мы пошли думать.
Разбираем идеи
Идея первая: настройка сборщика (спойлер: рассматривалась, но была отвергнута)
Суть идеи заключалась в реализации настройки сборщика так, чтобы он собирал только тот кусок кода, который необходим, и настроить нужные точки входа.
Однако сразу столкнулись с минусами этого решения:
Достоинства |
Недостатки |
Простота начальной настройки. |
Жёсткая привязка к сборщику. Сложность масштабирования. Проблемы с изоляцией зависимостей. |
Идея вторая: клонирование логики
Клонирование логики в отдельное приложение сразу ушло в «топку» — поддержка двух сервисов (тестирование и разработка) очень накладна.
Идея третья: микрофронтовое приложение (она и была выбрана для реализации)
Я подумал: «А почему не сделать микрофронтовое приложение, которое будет встраиваться и в наш хост, и передаваться в другие хосты с соблюдением необходимых контрактов?». Не увидел причин не попробовать.
Приступим к реализации
На первый взгляд это казалось накладным и трудозатратным, но отступать было поздно.
Стартовый репозиторий и коммит.
Подготовка репозитория
Неважно, монолит фронта у вас или набор сервисов как у нас — структура меняется только в директории фронта (без названия папки вашего фронта).

Почему важно сразу подготовить репозиторий
Правильная структура для микросервисной архитектуры:
не будет кросс-зависимостей в модулях;
изоляция модулей и пакетов;
не будет конфликтов при работе в большой команде и при параллельной разработке.
Планирование микрофронтов
Микрофронтовые приложения будут реализованы как ES-модули. Ниже описаны краткие преимущества:
Нативная изоляция и отсутствие конфликтов глобальных переменных: каждый микрофронтенд, собранный как ESM, работает в собственном пространстве имен. В отличие от CommonJS или глобальных скриптов (IIFE), модули не засоряют
windowи не конфликтуют версиями библиотек. Это решает проблему «войны зависимостей» между командами.Мгновенная сборка и загрузка (Fast Refresh и Instant Loading): ES-модули — это нативная фича браузера. Вы можете подавать их без бандлера (или с минимальной транспиляцией) в dev-режиме. Это дает мгновенный HMR (Hot Module Replacement) — изменение в любом микрофронтенде сразу отображается без пересборки всего приложения.
Древовидная тряска (Tree Shaking) на уровне фреймворка: современные сборщики (Vite, Webpack 5) отлично анализируют ESM. Если ваша общая библиотека (например, моментные функции) не используется в конкретном микрофронтенде, она не попадет в его бандл. Для
importэто работает гораздо точнее, чем дляrequire.Упрощенная версионность и кэширование (HTTP Cache): можно экспортировать свой микрофронтенд как ESM-файл
app.mjs. Браузеры кэшируют такие модули по стандарту HTTP. При изменении кода меняете только URL или версию вimportmap— пользователь догружает только измененную часть, а не весь монолит.Естественная поддержка динамических импортов (Dynamic Import): микрофронтенды, собранные как ESM, позволяют использовать
import()для «ленивой» загрузки по требованию. Это дает нативныйcontrol-flowзагрузки без велосипедов с<script>тегами и событиямиonload.Единый «контракт» для разных фреймворков: ESM — это универсальный формат. Если один микрофронтенд написан на Svelte, другой на React, а третий на Vue — все они могут «договориться» через ESM-экспорты, передавая друг другу данные или
shared dependencies.Меньше накладных расходов на интеграцию (No Shell Overhead): в системах вроде Module Federation или iframe вам нужен «контейнер-мост». С ESM подключается модуль в host-приложение через
<script type="module">или черезimport map— никакой лишней очереди сообщений (postMessage) или прокси-библиотек.
Для полной функциональности нам требуется контракт общения между хост-сервисом и микрофронтовым приложением. Чтобы в каждом сервисе и в хосте не клонировать одни и те же библиотеки и общие сущности, нужна ещё одна общая библиотека для использования и в хосте, и в сервисах.
План действий
Сначала реализуем общую библиотеку компонентов и утилит, чтобы сразу перенести на неё наш хост, протестировать и после проверки двигаться дальше. По формированию библиотеки — решать вам: сделать несколько библиотек под нужды или создать одну общую с компонентами, утилитами, провайдерами и так далее. Мы сделали одну общую для более точной поддержки и версионирования.
Посмотрите на коммит с реализацией библиотеки.
Общую библиотеку ставим из tgz-файла. Зачем? Чтобы версионировать её и более мягко обновляться в сервисах и хосте (при правильном и возможном варианте, библиотеку можно вынести в артифактори, например npmrs/nexus, но у нас есть свои нюансы и выход – это хранить пакеты локально).
Далее формируем контракт общения, модуль загрузки микрофронта и всё, что необходимо для этого.
В зависимости от требований к проекту, наш контракт выглядит так (в коде контракт будет пустым, как абстракция):
Контракт микрофронта
В таблице ниже описан минимальный интерфейс для взаимодействия хоста и микрофронта.
Параметр |
Описание |
|
Текущая тема проекта |
|
Текущий язык проекта |
|
Функция добавления подписчика на изменение темы хост‑приложения |
|
Функция удаления подписчика на изменение темы хост‑приложения |
|
Функция добавления подписчика на изменение языка хост‑приложения |
|
Функция удаления подписчика на изменение языка хост‑приложения |
Сборка и загрузка микрофронта
Теперь подробнее рассмотрим вопросы: как правильно собрать и как правильно загрузить микрофронт.
Сначала реализуем (без удаления в хост-приложении) перенос микрофронта в папку apps. Проверим, что он работает как самостоятельное приложение. Далее настроим сборку приложения как микрофронтового ES-модуля.
Детальный процесс показан на диаграмме.

Конфигурация Vite для микрофронта (vite.config.mfe.mts)
Код:
/* eslint-disable no-magic-numbers */ import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import { checker } from 'vite-plugin-checker'; import EnvironmentPlugin from 'vite-plugin-environment'; import * as path from 'path'; /** * MFE-сборка self-contained: Все зависимости (включая React/ReactDOM/emotion) * собираются внутрь одного файла entry-[hash].js. */ export default defineConfig({ plugins: [ react({ jsxImportSource: '@emotion/react' }), EnvironmentPlugin('all'), checker({ typescript: true, terminal: true }), ], envPrefix: 'KINTSUGI', base: '/', optimizeDeps: { include: ['react', 'react-dom', '@kintsugi/uikit', '@emotion/react', '@emotion/styled'], exclude: [], force: true, }, build: { target: 'esnext', outDir: path.resolve(__dirname, 'build-mfe'), assetsDir: '', sourcemap: process.env.NODE_ENV === 'production' ? false : 'inline', manifest: true, cssCodeSplit: true, commonjsOptions: { include: [/node_modules/], sourceMap: false, transformMixedEsModules: true, }, minify: 'terser', terserOptions: { compress: { drop_console: true }, }, rolldownOptions: { external: [], preserveEntrySignatures: 'strict', input: './src/app/mfe-entry.tsx', output: { format: 'es', inlineDynamicImports: true, entryFileNames: 'entry-[hash].js', assetFileNames: 'assets/[name]-[hash].[ext]', }, }, }, resolve: { alias: { app: path.resolve(__dirname, 'src/app/'), pages: path.resolve(__dirname, 'src/pages/'), widgets: path.resolve(__dirname, 'src/widgets/'), features: path.resolve(__dirname, 'src/features/'), entities: path.resolve(__dirname, 'src/entities/'), shared: path.resolve(__dirname, 'src/shared/'), }, }, });
Точка сборки микрофронта (mfe-entry.tsx)
Код:
import createCache from '@emotion/cache'; import { CacheProvider } from '@emotion/react'; import { createRoot, type Root } from 'react-dom/client'; import { App } from './app'; import type { MFEModule, MFEMeta } from '@fsd/mfe-shell'; export const meta: MFEMeta = { name: 'copywala', version: '1.0.0', apiVersion: 1, }; const roots = new WeakMap<HTMLElement | ShadowRoot, Root>(); export const mount: MFEModule['mount'] = (root, ctx) => { let reactRoot = roots.get(root); if (!reactRoot) { reactRoot = createRoot(root as Element); roots.set(root, reactRoot); } const cache = createCache({ key: 'cw', container: root as unknown as Node, prepend: true, }); reactRoot.render( <CacheProvider value={cache}> <App /> </CacheProvider>, ); const unmount = () => { const r = roots.get(root); if (!r) return; r.unmount(); roots.delete(root); }; ctx.signal.addEventListener('abort', unmount, { once: true }); return unmount; }; export default App;
Скрипт сборки микросервисов
Код:
import { execSync } from "child_process"; import { cpSync, mkdirSync, writeFileSync, existsSync, rmSync, readFileSync, readdirSync, statSync } from "fs"; import * as path from "path"; const SERVICES = ["logDashboard"]; const HOST_PUBLIC = path.resolve("..", "host/public"); const MICRO_DIR = path.resolve("..", "microservices"); const MANIFEST_INPUT_PATH = path.resolve(HOST_PUBLIC, "manifest.json"); if (existsSync(MICRO_DIR)) rmSync(MICRO_DIR, { recursive: true }); let manifest = { version: "1.0.0", services: [] }; if (existsSync(MANIFEST_INPUT_PATH)) { try { const rawData = readFileSync(MANIFEST_INPUT_PATH); const parsedData = JSON.parse(rawData); manifest = { ...parsedData, services: [] }; } catch (error) { console.warn("Ошибка парсинга manifest.json. Используется шаблон по умолчанию:", error.message); } } for (const name of SERVICES) { try { execSync(cd ${name} && rm -rf node_modules && npm i --legacy-peer-deps && npm run build:mfe, { stdio: "inherit" }); const srcBuildDir = path.resolve(${name}/build-mfe); const dest = path.resolve(MICRO_DIR); mkdirSync(dest, { recursive: true }); const esFiles = readdirSync(srcBuildDir).filter((f) => f.endsWith(".js")); for (const fileName of esFiles) { const srcFile = path.resolve(srcBuildDir, fileName); const destFile = path.resolve(${dest}/${name}/, fileName); if (statSync(srcFile).isFile()) { cpSync(srcFile, destFile); } manifest.services.push({ name, entry: /microservices/${name}/${fileName}, status: "active" }); } } catch (e) { console.error(Ошибка сборки ${name}:, e.message); process.exit(1); } } writeFileSync(path.resolve(HOST_PUBLIC, "manifest.json"), JSON.stringify(manifest, null, 2));
Скрипт выполняет следующее: запускает сборку сервисов из конфига SERVICES (временная константа, пока нет файла конфигурации от пайплайна). Удаляет информацию о ранее смонтированных сервисах в хостовом манифесте, собирает микросервис и при успехе копирует chunk в папку микросервисов, прописывая путь до него в манифесте.
Плагин копирования микросервисов (microservicesCopyPlugin)
Код:
function microservicesCopyPlugin(): Plugin { const microservicesSrc = path.resolve(__dirname, '../apps/microservices'); let buildOutDir: string; return { name: 'microservices-copy-plugin', apply: 'build', configResolved(config) { buildOutDir = config.build.outDir; }, closeBundle() { if (!existsSync(microservicesSrc)) return; const microservicesDest = path.resolve(buildOutDir, 'microservices'); mkdirSync(microservicesDest, { recursive: true }); const serviceDirs = readdirSync(microservicesSrc); for (const dirName of serviceDirs) { const fileNames = readdirSync(${microservicesSrc}/${dirName}); for (const fileName of fileNames) { if (fileName.endsWith('.js')) { const srcFile = path.resolve(${microservicesSrc}/${dirName}, fileName); const destFile = path.resolve(${microservicesDest}/${dirName}, fileName); if (statSync(srcFile).isFile()) { cpSync(srcFile, destFile); } } } } }, }; }
Интеграция микрофронта в хост
Мы настроили всё и готовы к интеграции. Теперь разберём, как хост будет загружать наш сервис.
Реализован компонент, который загружает все микрофронтовые приложения. Он загружает manifest.json, смотрит где находится микрофронт, и загружает его. В текущем DOM-узле формируется Shadow DOM и передаётся микрофронту как точка монтирования вместе со всеми нужными свойствами.
Дополнительные возможности при загрузке
Состояние загрузки и ошибки в микрофронтовых приложениях.
Состояния ошибок роутинга и монтирования приложения.
При загрузке микрофронтов в режиме разработки требуется изменить путь до папки микрофронтов на локальную папку
distилиbuild.
Преимущества монорепозитория с микрофронтами
Категория |
Описание |
Простота оркестрации |
Все сервисы в одном репозитории — единый CI/CD, общий код, легко развёртывать через |
Общий |
Один набор линтеров, тестов, |
Разделение бандла |
Каждый сервис — свой |
Типобезопасность |
Shared-пакеты с типами легко шарить между сервисами через |
Быстрый старт |
Не нужно поднимать отдельные репозитории, настраивать легенды и права доступа. |
Недостатки монорепозитория с микрофронтами
Категория |
Описание |
Рост связности |
Shared-код может незаметно связывать сервисы, увеличивая coupling. |
Общий бандл-сайз |
Если не контролировать импорты, можно случайно натащить лишнего в каждый chunk. |
Сложность изоляции |
Сервисы делят |
Масштабирование команд |
При росте команд более 10 разработчиков появляются конфликты в общем репозитории. |
Технический долг |
Легаси‑костыли копятся в shared‑пакетах, если не чистить регулярно. Длительность сборки. При монолитном CI каждый пуш может триггерить пересборку всех сервисов. |
Преимущества truly изолированных микрофронтов
Категория |
Описание |
Технологическая гибкость |
Можно использовать React, Vue, Svelte или другой фреймворк независимо от хоста. |
Independent Deploy |
Релиз микрофронта не требует развёртывания хоста. |
Многократное использование |
Один модуль подключается в несколько хост‑приложений без копирования кода. |
Independent Scaling |
Hot‑path микрофронт масштабируется отдельно от всего приложения. Fault Isolation. Падение микрофронта не ложит всё приложение (при грамотной обработке ошибок). |
Вызовы при интеграции микрофронта в хост
Категория |
Описание |
Разные версии зависимостей |
Хост и микрофронт могут тянуть разные версии React, что требует shared scope. Сложность отладки. Проблемы на стыке хоста и микрофронта труднее диагностировать. |
Заключение
Переход на микрофронтовую архитектуру — это стратегическое решение, которое требует тщательного планирования и понимания как преимуществ, так и вызовов.
Ключевые выводы:
Микрофронтовая архитектура обеспечивает изоляцию и независимость команд.
Монорепозиторий упрощает начальную настройку и поддержку общих библиотек.
ES-модули позволяют использовать lazy loading и reduce bundle size.
Необходимость чётких контрактов между хостом и микрофронтами.
Рекомендации для команд:
Начните с пилотного проекта — выделите один не критичный модуль.
Инвестируйте в общие библиотеки и типы с самого начала.
Настройте CI/CD для автоматической версионности shared-пакетов.
Документируйте контракты между сервисами как часть процесса разработки.
Планируйте регулярную очистку
shared-пакетов от технического долга.
Спасибо!