Привет, друзья! Меня зовут Наталья и я frontend‑разработчик в компании SimbirSoft.
Когда‑то в 2020 году микрофронтенды были модой. Чуть ли ни каждый хотел почувствовать себя всемогущим и попробовать разделить монолит на части, а также получить от шефа премию или эмоциональное поглаживание и картонку с надписью «Спасибо» (если повезет, то отдадут прямо с рамкой). Затем забыли про них и, но недавно в компанию пришла потребность разработки сайта с архитектурным паттерном «Микрофронтенды». Всех обязали вспомнить или узнать что же такое микрофронтенды? Чушь или все‑таки необходимость? Давайте разбираться.
По мере роста проектов мне как Vue‑разработчику уже неоднократно приходилось сталкиваться с вопросом: как разделить фронтенд между несколькими командами так, чтобы они меньше зависели друг от друга?
Одним из вариантов остаются микрофронтенды. В статье я разберу Module Federation 2.0 для Vue 3 + Vite, изоляцию стилей и состояния, способы коммуникации между remotes и то, что происходит с этой архитектурой, когда в проекте появляется SSR и Nuxt 4.
Статья будет полезна разработчикам и техлидам, которые рассматривают разделение крупного фронтенда между несколькими командами. А если вы пока только знакомитесь с микрофронтендами, основные понятия я тоже постараюсь объяснить по ходу статьи.
Сначала разберём Module Federation для Vue 3 + Vite, затем — изоляцию стилей и состояния, коммуникацию между приложениями, SSR в Nuxt 4 и роль host‑приложения в маршрутизации.
В конце оставлю короткий чек‑лист, по которому можно оценить, готов ли проект к такому переезду. Спойлер: микрофронтенды — это не «следующий уровень» архитектуры, а инструмент с довольно конкретной областью применения. И понимать, когда он не нужен, не менее важно, чем уметь его настроить.
Содержание:
Module Federation 2.0 в Vue 3 + Vite
Module Federation начинался как технология из экосистемы Webpack, но сегодня его runtime и инструменты живут отдельным слоем: есть интеграции для Rspack, Webpack и Vite. Для Vue‑проектов это важно прежде всего потому, что федерацию можно использовать, не отказываясь от Vite.
В феврале 2026 года Module Federation 2.0 получил статус stable, поэтому дальше я буду рассматривать уже актуальную стабильную ветку экосистемы.
Что изменилось в 2.0
Одна из ключевых вещей в современной экосистеме Module Federation — manifest с метаданными о remote: exposes, shared‑зависимостях и ассетах. Он используется не только для загрузки remote, но и для диагностики, preload и DevTools. Из этого вырастают несколько полезных возможностей:
Динамические remotes — runtime API позволяет регистрировать и переопределять remotes во время выполнения. Если список remotes хранится только в build‑конфигурации host, его изменение по‑прежнему потребует пересборки.
Type sharing — Module Federation умеет генерировать типы producer'а и подтягивать их в consumer. Для Vite‑интеграции remote types могут загружаться автоматически, а поведение генерации и потребления настраивается через dts.
Версионирование shared‑зависимостей — для shared можно задать singleton и requiredVersion, а runtime применяет эти правила при выборе общей версии.
DevTools‑расширение для Chrome — видно, какой remote что загрузил и какие версии shared активны.
Как это устроено
Концептуально схема остается прежней.
Host (shell) — приложение, которое подключает удаленные модули.
Remotes — независимо собираемые и деплоящиеся приложения или модули, экспортирующие компоненты, composables, stores или роуты.
Shared — зависимости, использование которых host и remotes согласуют между собой, например Vue, Pinia или Vue Router.
Host может подключаться не напрямую к remoteEntry.js, а через mf‑manifest.json. URL манифеста при этом всё равно задается в конфигурации; для действительно динамической регистрации remotes используется runtime API.
Конфигурация: Vite‑стек
Remote
// apps/checkout/vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { federation } from '@module-federation/vite' export default defineConfig({ plugins: [ vue(), federation({ name: 'checkout', filename: 'remoteEntry.js', manifest: true, exposes: { './CheckoutPage': './src/pages/CheckoutPage.vue', './useCart': './src/composables/useCart.ts', }, shared: { vue: { singleton: true, requiredVersion: '^3.5.0', }, pinia: { singleton: true, }, }, }), ], build: { target: 'esnext', }, })
exposes — список модулей, которые remote публикует наружу. Если host и remotes должны работать в одном Vue‑контексте, Vue обычно настраивают как singleton: иначе можно получить несколько runtime‑копий и проблемы с provide/inject или реактивным контекстом.
Host
// apps/shell/vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { federation } from '@module-federation/vite' export default defineConfig({ plugins: [ vue(), federation({ name: 'shell', remotes: { checkout: { type: 'module', name: 'checkout', entry: 'https://checkout.example.com/mf-manifest.json', }, }, shared: { vue: { singleton: true, requiredVersion: '^3.5.0', }, pinia: { singleton: true, }, }, }), ], })
Использование remote‑компонента:
// apps/shell/src/router/index.ts import { createRouter, createWebHistory, } from 'vue-router' export const router = createRouter({ history: createWebHistory(), routes: [ { path: '/checkout', component: () => import('checkout/CheckoutPage'), }, ], })
Импорт 'checkout/CheckoutPage' — виртуальный путь remote‑модуля. Module Federation разрешает его через конфигурацию remote и загружает модуль во время выполнения. Для Vue Router это обычный lazy‑loaded route component.
Подводные камни
CORS на federation‑ресурсах. Если host и remote размещены на разных origin, сервер remote должен возвращать подходящие CORS‑заголовки для mf‑manifest.json, remoteEntry.js и загружаемых чанков. Это одна из первых вещей, которые стоит проверить при сетевой ошибке загрузки remote.
Singleton‑зависимости. Если host и remotes должны работать с одной копией Vue, это нужно одинаково зафиксировать в shared‑конфигурации. Иначе можно получить несколько runtime‑копий Vue и очень неприятные ошибки на границах provide/inject и общего состояния. Здесь помогают code review и проверка конфигурации.
Кеширование manifest и remoteEntry. Не стоит агрессивно кешировать mf‑manifest.json: после деплоя host должен быстро увидеть актуальный manifest. Для долгоживущих ассетов лучше использовать версионированные или хешированные URL и корректные Cache‑Control‑заголовки.
Размер shared‑зависимостей. Не нужно автоматически помещать в shared всё подряд. Singleton имеет смысл там, где действительно нужен единый экземпляр или общий runtime‑контекст, например для Vue; остальные зависимости стоит оценивать отдельно по размеру и требованиям к версиям.
При проблемах с federation‑конфигурацией я бы сначала проверяла именно эти четыре места: CORS, singleton‑настройки, кеширование и состав shared‑зависимостей.
А что насчет Native Federation?
Native Federation — browser‑native реализация ментальной модели Module Federation, построенная вокруг стандартных ES Modules и Import Maps. В ее экосистеме @softarc/native-federation — core: он отвечает за build‑time часть и не привязан к конкретному фреймворку или сборщику. Интеграция со сборщиком выполняется через adapter layer.
Для Vite путь есть, но здесь важна оговорка: в текущей документации reference adapters отдельно перечислены для esbuild и Angular, а для Vite указан community plugin. Поэтому для Vue 3 + Vite это менее прямой вариант, чем @module-federation/vite, и в этой статье я не буду разбирать его подробно.
Изоляция стилей и состояния
Подключить remote — только половина задачи. После этого приходится решать, как приложения будут делить стили и состояние и как искать источник ошибки, если проблема проявляется уже в общем интерфейсе. В Vue‑проектах для этого обычно комбинируют scoped‑стили, CSS Layers, Custom Elements и явно определенные контракты состояния.
Изоляция стилей, уровень 1: scoped + CSS Layers
<style scoped> даёт префиксы атрибутов, но не спасает от глобальных селекторов вроде body или *. CSS Layers (@layer) позволяет явно определить приоритет источников:
@layer reset, remotes, shell; @import url('checkout.css') layer(remotes); @import url('catalog.css') layer(remotes); @layer shell { /* глобальные стили host-приложения */ }
Для обычных деклараций слой shell имеет более высокий приоритет, потому что он объявлен после remotes. Это позволяет host переопределять правила remote даже при более низкой специфичности. Но @layer не создаёт настоящую CSS‑изоляцию: глобальные селекторы remote всё ещё могут влиять на страницу, а для!important порядок слоёв работает наоборот.
Изоляция стилей, уровень 2: Shadow DOM через defineCustomElement
Если нужна более жёсткая граница стилей, Vue позволяет упаковать компонент в Custom Element с Shadow DOM:
import { defineCustomElement } from 'vue' import CheckoutPage from './CheckoutPage.ce.vue' const CheckoutElement = defineCustomElement(CheckoutPage) customElements.define('mfe-checkout', CheckoutElement)
В host такой remote монтируется как <mfe‑checkout></mfe‑checkout>. Shadow DOM изолирует внутреннее DOM‑дерево и большую часть стилей компонента. Данные внутрь Custom Element можно передавать через props и DOM properties, а события наружу — через Custom Events. При этом provide/inject обычного host‑приложения через границу отдельного Custom Element автоматически не проходит.
Изоляция стилей, уровень 3: iframe
iframe дает наиболее жесткую изоляцию и подходит для legacy‑приложений или частей системы, которым нужна отдельная среда выполнения. Цена — более сложная интеграция навигации, размеров, событий и общей авторизации.
Изоляция состояния: Pinia в изолированном контексте
В простой реализации каждый remote делает createPinia() сам и получает свой экземпляр Pinia. Это уже хорошая изоляция. Проблемы начинаются, когда нужно поделиться состоянием (например, текущим пользователем) между host'ом и remotes.
Если remote‑компонент рендерится внутри того же Vue app context, локальные stores можно оставить внутри remote, а действительно общее состояние передавать из host через явно определенный контракт. Для этого подходит provide/inject. Контракт лучше вынести в отдельный shared‑пакет, чтобы remote не импортировал типы напрямую из реализации shell.
// @org/contracts/user.ts import type { InjectionKey } from 'vue' export interface SharedUserStore { userId: string | null logout(): void } export const sharedUserStoreKey: InjectionKey<SharedUserStore> = Symbol('sharedUserStore') // В host import { sharedUserStoreKey } from '@org/contracts/user' const userStore = useUserStore() app.provide(sharedUserStoreKey, userStore) // В remote import { inject } from 'vue' import { sharedUserStoreKey } from '@org/contracts/user' export function useSharedUser() { const store = inject(sharedUserStoreKey) if (!store) throw new Error('sharedUserStore not provided by host') return store }
Так remote зависит только от общего контракта, а не от конкретной реализации store в shell. Этот подход работает, когда remote‑компонент находится в том же Vue app context; для отдельно смонтированного приложения или Custom Element границу состояния нужно проектировать отдельно.
Подводные камни
CSS Layers и!important. Для обычных деклараций более поздний слой имеет больший приоритет, а для!important порядок слоев инвертируется. Поэтому использование!important лучше ограничивать lint‑правилами и отдельно учитывать его при проектировании порядка слоев.
Shadow DOM + сторонние UI‑киты. У библиотек, которые рендерят модалки и dropdown через Teleport в document.body, могут возникнуть проблемы со стилями и контекстом, потому что Teleport выходит за границу Shadow DOM. Перед миграцией нужно проверить возможность настроить target/appendTo и поведение конкретной библиотеки.
provide/inject через границу remote. Недостаточно только общего Vue runtime: компонент remote должен находиться в том же Vue app context. Для отдельно смонтированного Vue‑приложения или отдельного Custom Element обычный provide из host автоматически недоступен.
SSR и Custom Elements. Vue Custom Elements с Shadow DOM не стоит считать универсальным SSR‑решением для этой архитектуры. Если SSR критичен, серверный рендеринг и гидратацию такого remote нужно проектировать и проверять отдельно; для SEO‑критичных частей обычно проще оставить обычные Vue/Nuxt‑компоненты.
Коммуникация между микрофронтендами
Микрофронтендам всё равно приходится обмениваться данными, но границы этого обмена лучше делать явными. Чем больше прямых импортов и неформальных связей между remotes, тем быстрее независимый деплой превращается в формальность. Ниже покажу несколько вариантов коммуникации от браузерных событий до общей шины.
Custom Events для границы Shadow DOM
Если remote оформлен как Custom Element с Shadow DOM, Custom Events — удобный способ отправлять события наружу через границу Shadow DOM:
// В remote import { useHost } from 'vue' const host = useHost() function completeCheckout() { host?.dispatchEvent( new CustomEvent('checkout:completed', { detail: { orderId: '12345', total: 4990 }, bubbles: true, composed: true, }), ) } // В host import { onMounted, onUnmounted } from 'vue' function handleCheckoutCompleted(event: Event) { const { orderId } = (event as CustomEvent<{ orderId: string; total: number }>).detail analytics.track('order_completed', { orderId }) } onMounted(() => { document.addEventListener('checkout:completed', handleCheckoutCompleted) }) onUnmounted(() => { document.removeEventListener('checkout:completed', handleCheckoutCompleted) })
Плюсы: нативный браузерный механизм и отсутствие дополнительной runtime‑зависимости.
Минусы: модель «отправил событие и забыл» не даёт встроенного request/response, а контракт события и его типизацию нужно поддерживать самостоятельно.
BroadcastChannel для коммуникации между вкладками
Когда нужно общаться между вкладками, окнами или iframe одного origin:
import { onUnmounted } from 'vue' const channel = new BroadcastChannel('app-state') channel.postMessage({ type: 'user:logout' }) channel.onmessage = (event) => { if (event.data.type === 'user:logout') { userStore.reset() } } onUnmounted(() => { channel.close() })
Особенно полезно для синхронизации авторизации между вкладками.
Mitt как типизированная шина
Для простой событийной шины внутри одного приложения можно использовать mitt — небольшой pub/sub без сложного runtime:
// @org/events/bus.ts import mitt, { type Emitter } from 'mitt' export type AppEvents = { 'cart:item-added': { productId: string; qty: number } 'cart:cleared': void 'user:logged-in': { userId: string } } export const bus: Emitter<AppEvents> = mitt<AppEvents>() // В remote A bus.emit('cart:item-added', { productId: 'sku-123', qty: 2 }) // В remote B bus.on('cart:item-added', ({ productId, qty }) => { /* реагируем */ })
busдолжен быть singleton через тот же механизм, что и Vue, иначе у каждого remote будет своя шина. В MF 2.0: shared: { '@org/events': { singleton: true } }.
Для простых событий mitt обычно достаточно. Более тяжелый инструмент нужен не «по мере роста проекта», а когда сама модель взаимодействия становится потоковой и требует операторов, replay или композиции нескольких источников.
RxJS для сложных сценариев
Когда появляются зависимости между событиями, композиция потоков, throttle/debounce или replay, простой event bus быстро становится неудобным. В таких сценариях уже имеет смысл смотреть в сторону RxJS:
import { Subject, BehaviorSubject } from 'rxjs' export const cartAdded$ = new Subject<{ productId: string; qty: number }>() export const currentUser$ = new BehaviorSubject<User | null>(null)
BehaviorSubject удобен, когда новому подписчику нужно сразу получить последнее значение. Например, remote может загрузиться позже остальных и всё равно получить актуального пользователя, а не ждать следующего события.
Подводные камни
Event‑storm. Если через шину начинают передавать всё подряд, со временем становится трудно понять, кто публикует и кто слушает конкретное событие. Помогают явный список публичных событий в shared‑пакете, code review и версионирование контрактов.
Singleton шины. Если разные remotes получают разные экземпляры пакета с event bus, они фактически работают с разными шинами. Поэтому общий bus нужно действительно разделять как один экземпляр либо передавать через явный host‑контракт.
Memory leaks через подписки. Подписки на mitt или RxJS нужно очищать при размонтировании компонента или приложения. Иначе обработчики продолжают жить дольше самого remote.
Контракт событий — часть публичного API между командами. Изменение имени события или payload лучше проводить так же аккуратно, как изменение HTTP‑контракта: с версионированием или переходным периодом.
SSR и микрофронтенды в Nuxt 4
Сочетать runtime‑микрофронтенды с SSR заметно сложнее, чем подключать их только на клиенте. Нужно согласовать серверную загрузку remote, передачу состояния и последующую гидратацию на клиенте. В экосистеме Nuxt для этого появляются отдельные интеграции и экспериментальные подходы.
Что даёт Nuxt 4
Layers — через extends можно подключать общий слой с компонентами, composables, middleware и конфигурацией. Это build‑time composition: независимого runtime‑деплоя remote здесь нет, зато интеграция заметно проще.
Runtime Module Federation + SSR — отдельная интеграционная задача. В актуальной документации
@module-federation/viteподдержка Nuxt SSR всё ещё указана в roadmap, поэтому этот сценарий нельзя подавать как готовый стандартный путь для Nuxt 4.<NuxtIsland>— механизм Nuxt для server components/islands. Он полезен для серверной композиции, но сам по себе не делает Module Federation remote SSR‑микрофронтендом и не означает отдельный клиентский «гидрационный цикл» для каждого remote.
Экспериментальные варианты SSR
Runtime Module Federation вместе с SSR в Nuxt пока стоит считать отдельной интеграционной задачей, а не готовым универсальным паттерном. Официальная документация @module‑federation/vite по‑прежнему относит Nuxt SSR к roadmap, поэтому конкретную схему нужно проверять на используемой версии и в реальном проекте.
При этом в Nuxt есть более предсказуемый build‑time вариант — Layers. Через extends можно вынести общие компоненты, composables, middleware и конфигурацию в отдельный слой и подключать его в несколько приложений без runtime‑загрузки remote‑модулей.
<NuxtIsland> решает другую задачу: он рендерит server component как отдельный остров. Сам по себе он не превращает remote из Module Federation в SSR‑микрофронтенд, а <ClientOnly> наоборот исключает вложенный компонент из серверного рендера.
Поэтому для SEO‑критичных частей я бы сначала рассматривала Nuxt Layers или обычную серверную композицию. Runtime federation + SSR имеет смысл добавлять только тогда, когда независимый деплой remote действительно важнее простоты серверного рендера.
Ограничения и компромиссы
SSR заметно повышает сложность микрофронтендной архитектуры, поэтому его стоит добавлять только там, где он действительно дает продуктовую пользу.
Удаленный SSR добавляет сетевые зависимости в критический путь рендера. Запросы можно выполнять параллельно, поэтому задержка не обязана расти линейно с количеством remotes, но tail‑latency и вероятность частичных отказов становятся выше.
Версии Vue и связанных библиотек нужно согласовывать и на клиенте, и на сервере. Клиентский singleton решает только часть задачи: серверная модель зависит от того, как именно организованы процессы, сборка и загрузка remotes.
State serialization. Состояние, которое передается с сервера на клиент, должно корректно сериализоваться и восстанавливаться. Функции, классы и циклические ссылки требуют отдельной обработки и могут приводить к ошибкам гидратации.
Shadow DOM и Custom Elements не стоит считать универсальной SSR‑границей. Если этот подход нужен вместе с SSR, серверный вывод и клиентскую активацию придётся проектировать отдельно; для обычных Nuxt‑компонентов проще использовать CSS‑изоляцию на уровне приложения.
Не все remotes нужно рендерить на сервере. Для аналитики, части виджетов и интерактивных инструментов client‑only может быть достаточен. SSR имеет смысл там, где он улучшает SEO, первоначальный контент или пользовательское восприятие загрузки.
Подводные камни
SSR “на всякий случай”. Лучше отдельно определить, какие части продукта действительно выигрывают от серверного рендера. Для остальных client‑side загрузка может оказаться проще и надёжнее.
Кеширование SSR‑вывода. Общий для пользователей HTML можно кешировать на сервере или CDN. Персонализированный вывод требует более осторожной стратегии кеширования и корректного разделения ключей.
Логирование через границу. Ошибка remote во время SSR может проявиться уже на стороне host, поэтому полезно передавать единый request/trace id и сохранять контекст ошибки в обеих системах.
window и document в remote-коде. Браузерные API недоступны во время серверного рендера, поэтому такой код нужно выполнять только на клиенте, например, через import.meta.client или lifecycle‑хуки, которые запускаются после монтирования.
Оркестрация и роутинг на уровне host‑приложения
На host обычно остаются верхнеуровневый роутинг, загрузка remote‑модулей, обработка ошибок и общие элементы интерфейса. Для route‑компонентов Vue Router 4 достаточно dynamic import. Если remote нужен отдельный loading/error UI, defineAsyncComponent удобнее вынести внутрь локального wrapper‑компонента.
Vue Router 4 как точка оркестрации
Базовая идея: host оставляет за собой верхнеуровневый URL и лениво загружает wrapper‑компонент нужного раздела:
// apps/shell/src/router/index.ts import { createRouter, createWebHistory } from 'vue-router' export const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('@/pages/Home.vue'), }, { path: '/catalog/:pathMatch(.*)*', component: () => import('@/pages/CatalogRoute.vue'), }, ], })
В самом wrapper‑компоненте можно отдельно управлять состояниями загрузки и ошибки remote:
<!-- apps/shell/src/pages/CatalogRoute.vue --> <script setup lang="ts"> import { defineAsyncComponent } from 'vue' import RemoteErrorBoundary from '@/components/RemoteErrorBoundary.vue' import RemoteLoading from '@/components/RemoteLoading.vue' const RemoteCatalog = defineAsyncComponent({ loader: () => import('catalog/CatalogApp'), loadingComponent: RemoteLoading, errorComponent: RemoteErrorBoundary, timeout: 10000, onError(_error, retry, fail, attempts) { if (attempts < 3) { retry() return } fail() }, }) </script> <template> <RemoteCatalog /> </template>
Ключевые моменты
pathMatch(.*)* удерживает все URL вида /catalog/... внутри одного маршрута host. Сам по себе catch‑all не передаёт остаток пути во второй Vue Router: способ передачи текущего пути зависит от того, как remote смонтирован и нужен ли ему вообще отдельный роутер.
timeout: 10 000 переводит async‑компонент в error state, если загрузка не завершилась за 10 секунд. Значение также получено из моего опыта, но никогда не бойтесь изменять его для вашего проекта.
Fallback‑стратегии при ошибках
Простой errorComponent может предложить перезагрузить страницу или вернуться в рабочую часть приложения:
<template> <div class="remote-error"> <h2>Раздел временно недоступен</h2> <button @click="reloadPage">Перезагрузить страницу</button> <RouterLink to="/">На главную</RouterLink> </div> </template> <script setup lang="ts"> function reloadPage() { window.location.reload() } </script>
Настоящий retry без полной перезагрузки лучше делать через callback retry из defineAsyncComponent.onError.
Для глобального логирования ошибок Vue можно использовать типобезопасный errorHandler:
app.config.errorHandler = (err, _instance, info) => { const message = err instanceof Error ? err.message : String(err) telemetry.track('vue_error', { error: message, info, path: router.currentRoute.value.fullPath, }) }
Ошибки навигации и загрузки route‑компонентов также полезно отслеживать через router.onError().
Размонтирование и очистка
Vue размонтирует сам компонент, но глобальные обработчики, созданные кодом remote, нужно очищать явно. Например:
onMounted(() => window.addEventListener('beforeunload', warnAboutUnsavedCart)) onUnmounted(() => window.removeEventListener('beforeunload', warnAboutUnsavedCart))
Подводные камни
Граница ответственности за routing. Если remote подключается как обычный route‑компонент, верхнеуровневую навигацию лучше оставлять host‑приложению. Отдельный router внутри remote имеет смысл только как осознанная архитектурная схема с явным контрактом синхронизации URL.
Кеширование federation entry. После деплоя важно, чтобы host не застревал на устаревшем manifest или remoteEntry. Конкретная стратегия зависит от CDN и схемы деплоя: обычно entry‑файлам задают более короткий кеш, а версионированным ассетам — долгий.
Скролл при переходах. Когда верхнеуровневой навигацией управляет host, scrollBehavior логичнее держать в его router. Если remote реализует собственную вложенную навигацию, ответственность за восстановление скролла нужно разделить явно.
Lazy loading и preload. Если по аналитике пользователи часто переходят в /catalog сразу после загрузки главной, remote можно заранее подгрузить в idle‑времени:
const preloadCatalog = () => { void import('catalog/CatalogApp') } if ('requestIdleCallback' in window) { window.requestIdleCallback(preloadCatalog, { timeout: 2000 }) } else { window.setTimeout(preloadCatalog, 1000) }
Код не попадет в initial bundle, но будет заранее загружен по сети после старта приложения. Такая оптимизация имеет смысл только если переход в /catalog действительно частый.
Другие решения, которые стоит знать
@originjs/vite-plugin-federation
Если нужен альтернативный federation‑плагин именно для Vite/Rollup, стоит знать про @originjs/vite-plugin-federation. Это отдельная реализация, вдохновлённая Webpack Module Federation; в документации есть готовый Vue 3 demo, а конфигурация строится прямо в vite.config.ts. Я бы рассматривала его прежде всего для существующих проектов или когда его API и ограничения лучше подходят конкретной задаче. В основной части статьи я его не использую, чтобы не смешивать API двух разных реализаций federation.
single-spa
single-spa решает немного другую задачу. Это не federation‑плагин, а оркестратор: он определяет, когда отдельное приложение должно bootstrap, mount и unmount. Для Vue есть официальный адаптер single-spa-vue, а в документации single-spa отдельно описана работа с Vite.
Такой подход интересен, когда микрофронтенды действительно являются самостоятельными приложениями, когда нужно постепенно мигрировать большой frontend или когда на одной странице должны сосуществовать части на разных фреймворках. При этом single-spa не заменяет Module Federation один в один: вопрос загрузки и шаринга модулей остаётся отдельной частью архитектуры.
В этой статье я сосредоточилась на @module-federation/vite, потому что для Vue 3 + Vite это наиболее прямой путь к runtime‑загрузке независимо деплоящихся частей. Но сама микрофронтендная архитектура Module Federation не ограничивается: в зависимости от задачи можно выбрать другую реализацию federation или orchestration‑подход вроде single-spa.
Заключение
Если ты дочитал до этого места — уже герой. Давай подытожим.
Самое главное, что микрофронтенды нужны командам, а не приложению. Ценность этого решения — не техническая, а организационная: возможность нескольким командам работать независимо.
И хотя инструменты создания архитектуры микрофронтендов на Vue стали более зрелые, но до появления единственного стандарта далеко. Выбор инструментария всегда остается за опытным разработчиком или даже за всей командой. Выбирай, то, что подойдет тебе, а не следуй веяниям моды.
Чек‑лист готовности к микрофронтендам
Команды. Есть как минимум две продуктовые команды, которым действительно мешают общий репозиторий, общий релизный цикл или постоянные блокировки друг друга? Если нет, возможно, микрофронтенды пока решают проблему, которой у проекта нет.
Дизайн‑система. Есть единый источник UI‑компонентов и дизайн‑токенов? Без общего визуального контракта независимо развивающиеся части интерфейса довольно быстро начинают расходиться.
DevOps‑готовность. Инфраструктура умеет независимо собирать, деплоить, версионировать и мониторить несколько приложений? Если CI/CD и наблюдаемость ещё нестабильны даже для монолита, микрофронтенды только умножат эту проблему.
Если на все три вопроса ответ «да», тогда уже есть смысл обсуждать конкретную архитектуру: с каких границ начать, какие части выделять первыми и как мигрировать без большого переписывания. На практике именно этот этап оказывается важнее выбора конкретного federation‑плагина.
Спасибо за внимание!
Больше авторских материалов для frontend‑разработчиков от моих коллег читайте в соцсетях SimbirSoft — ВКонтакте и Telegram.