Настройка Module Federation (Vite) для микрофронтенда
Vite нативно не поддерживает Module Federation — в отличие от Webpack 5. Решение — плагин @originjs/vite-plugin-federation (или vite-plugin-federation), который реализует тот же протокол, что и Webpack MF, с поддержкой обоих бандлеров на разных концах.
Есть ограничения: Vite в dev-режиме не поддерживает живую загрузку federated модулей так же гладко, как Webpack — remote нужно билдить перед использованием в host. Для production всё работает идентично.
Что входит в работу
Настройка vite-plugin-federation в host и remote приложениях, shared-зависимости, TypeScript, dev-workflow с pre-build remote, CI/CD независимый деплой, обработка ошибок, коммуникация между microfrontends.
Установка
npm install -D @originjs/vite-plugin-federation
vite.config.ts — Remote (catalog)
// apps/catalog/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import federation from '@originjs/vite-plugin-federation'
export default defineConfig({
plugins: [
react(),
federation({
name: 'catalog',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
'./ProductCard': './src/components/ProductCard',
'./useProducts': './src/hooks/useProducts',
},
shared: ['react', 'react-dom', 'react-router-dom'],
}),
],
build: {
target: 'esnext',
minify: false, // важно для federation
cssCodeSplit: false, // предотвращает проблемы с CSS в federated модулях
},
preview: {
port: 3001,
host: true,
},
})
vite.config.ts — Host (shell)
// apps/shell/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import federation from '@originjs/vite-plugin-federation'
const REMOTES = {
development: {
catalog: 'http://localhost:3001/assets/remoteEntry.js',
checkout: 'http://localhost:3002/assets/remoteEntry.js',
auth: 'http://localhost:3003/assets/remoteEntry.js',
},
production: {
catalog: 'https://catalog.example.com/assets/remoteEntry.js',
checkout: 'https://checkout.example.com/assets/remoteEntry.js',
auth: 'https://auth.example.com/assets/remoteEntry.js',
},
}
export default defineConfig(({ mode }) => ({
plugins: [
react(),
federation({
name: 'shell',
remotes: REMOTES[mode as keyof typeof REMOTES] ?? REMOTES.development,
shared: ['react', 'react-dom', 'react-router-dom'],
}),
],
build: {
target: 'esnext',
minify: false,
},
server: {
port: 3000,
},
}))
TypeScript-декларации для remote
Плагин не генерирует типы автоматически. Нужно либо публиковать типы через npm-пакет, либо описывать вручную:
// apps/shell/src/types/remotes.d.ts
declare module 'catalog/ProductList' {
import type { FC } from 'react'
interface Props {
categoryId: string
onProductClick?: (id: string) => void
}
const ProductList: FC<Props>
export default ProductList
}
declare module 'catalog/ProductDetail' {
import type { FC } from 'react'
interface Props {
productId: string
}
const ProductDetail: FC<Props>
export default ProductDetail
}
declare module 'checkout/CheckoutFlow' {
import type { FC } from 'react'
const CheckoutFlow: FC
export default CheckoutFlow
}
Более поддерживаемый подход — экспортировать типы из remote в виде npm-пакета:
packages/
catalog-types/
index.d.ts
package.json
// packages/catalog-types/index.d.ts
declare module 'catalog/ProductList' {
export { default } from '@catalog/types/components/ProductList'
}
Bootstrap pattern
// src/index.ts
import('./bootstrap')
// src/bootstrap.tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<App />
</BrowserRouter>
)
Использование remote в компоненте
// apps/shell/src/App.tsx
import React, { Suspense, lazy } from 'react'
import { Routes, Route } from 'react-router-dom'
import { RemoteErrorBoundary } from './components/RemoteErrorBoundary'
const ProductList = lazy(() => import('catalog/ProductList'))
const ProductDetail = lazy(() => import('catalog/ProductDetail'))
const CheckoutFlow = lazy(() => import('checkout/CheckoutFlow'))
const RemoteFallback = ({ name }: { name: string }) => (
<div>Приложение "{name}" временно недоступно</div>
)
function App() {
return (
<Routes>
<Route
path="/products"
element={
<RemoteErrorBoundary fallback={<RemoteFallback name="Каталог" />}>
<Suspense fallback={<PageSkeleton />}>
<ProductList categoryId="all" />
</Suspense>
</RemoteErrorBoundary>
}
/>
<Route
path="/products/:id"
element={
<RemoteErrorBoundary fallback={<RemoteFallback name="Каталог" />}>
<Suspense fallback={<PageSkeleton />}>
<ProductDetailWrapper />
</Suspense>
</RemoteErrorBoundary>
}
/>
</Routes>
)
}
Dev-workflow
Проблема Vite: remote нужно собрать перед запуском host в dev-режиме. Организуем через package.json в корне монорепо:
// package.json (monorepo root)
{
"scripts": {
"dev": "concurrently -n CATALOG,CHECKOUT,SHELL \"npm:dev:*\"",
"dev:catalog": "cd apps/catalog && vite build --watch",
"dev:checkout": "cd apps/checkout && vite build --watch",
"dev:shell": "cd apps/shell && vite",
"preview:catalog": "cd apps/catalog && vite build && vite preview",
"preview:checkout": "cd apps/checkout && vite build && vite preview"
}
}
npm install -D concurrently
Remote собираются в --watch режиме и сервируются через vite preview. Shell в обычном vite dev-режиме с HMR.
Динамические remotes
Когда URL remote нужно получить из конфигурации, а не захардкодить в vite.config:
// apps/shell/src/lib/dynamicImport.ts
export async function loadRemote<T>(
remoteName: string,
moduleName: string
): Promise<T> {
const config = await fetch('/api/mf-config').then((r) => r.json())
const remoteUrl = config[remoteName]
if (!remoteUrl) throw new Error(`Remote ${remoteName} not configured`)
// динамически загружаем remoteEntry
await new Promise<void>((resolve, reject) => {
const script = document.createElement('script')
script.src = remoteUrl
script.onload = () => resolve()
script.onerror = reject
document.head.appendChild(script)
})
// @ts-ignore — глобал, созданный remoteEntry
const container = window[remoteName]
await container.init(__webpack_share_scopes__.default)
const factory = await container.get(moduleName)
return factory()
}
Shared state
// packages/shared-store/index.ts
import { create } from 'zustand'
interface SharedState {
cart: CartItem[]
addToCart: (item: CartItem) => void
user: User | null
}
export const useSharedStore = create<SharedState>((set) => ({
cart: [],
addToCart: (item) => set((s) => ({ cart: [...s.cart, item] })),
user: null,
}))
// vite.config.ts (и host, и remotes)
shared: {
react: { requiredVersion: '^18.0.0' },
'react-dom': { requiredVersion: '^18.0.0' },
'@company/shared-store': { singleton: true, requiredVersion: '*' },
}
Что делаем
Настраиваем vite-plugin-federation в каждом приложении, организуем dev-workflow с --watch сборкой remotes, описываем TypeScript-декларации, настраиваем shared-зависимости без конфликтов версий, выстраиваем pipeline независимого деплоя через GitHub Actions + CDN.
Срок: 5–8 дней, включая настройку dev-окружения и CI/CD.







