Налаштування Module Federation (Webpack) для мікрофронтендів
Module Federation — вбудований механізм Webpack 5 для спільного використання коду між незалежними збірками в рантаймі. Кожен додаток (remote) публікує частину своїх модулів, інший (host) завантажує їх динамічно без перебудови. Код розгортається незалежно — зміни в remote негайно доступні в host.
Це не iframe та не web components — це справжній JS-код, що розділяє залежності (React, ReactDOM тощо) через механізм shared.
Що входить у роботу
Архітектурне проектування розбиття на remotes, налаштування ModuleFederationPlugin в кожному додатку, типізація через @module-federation/typescript, shared-залежності, динамічне завантаження, обробка помилок завантаження, CI/CD з незалежним розгортанням.
Архітектура
host (shell) — основний додаток, точка входу
├── remote: catalog — каталог продуктів
├── remote: checkout — оформлення замовлення
├── remote: profile — особистий кабінет
└── remote: auth — віджет авторизації (shared UI)
Кожен remote — окремий репозиторій з окремим CI/CD pipeline.
Встановлення
# в кожному додатку
npm install webpack@5 webpack-cli webpack-dev-server
npm install @module-federation/typescript
npm install html-webpack-plugin babel-loader @babel/core @babel/preset-react @babel/preset-typescript
webpack.config.js — Host (shell)
// apps/shell/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container
const HtmlWebpackPlugin = require('html-webpack-plugin')
const deps = require('./package.json').dependencies
module.exports = (env, argv) => ({
mode: argv.mode ?? 'development',
entry: './src/index.ts',
output: {
publicPath: 'auto',
filename: '[name].[contenthash].js',
clean: true,
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-typescript'],
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
catalog: `catalog@${
argv.mode === 'production'
? 'https://catalog.example.com'
: 'http://localhost:3001'
}/remoteEntry.js`,
checkout: `checkout@${
argv.mode === 'production'
? 'https://checkout.example.com'
: 'http://localhost:3002'
}/remoteEntry.js`,
auth: `auth@${
argv.mode === 'production'
? 'https://auth.example.com'
: 'http://localhost:3003'
}/remoteEntry.js`,
},
shared: {
react: {
singleton: true,
requiredVersion: deps.react,
eager: false,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
eager: false,
},
'react-router-dom': {
singleton: true,
requiredVersion: deps['react-router-dom'],
},
},
}),
new HtmlWebpackPlugin({ template: './public/index.html' }),
],
devServer: {
port: 3000,
historyApiFallback: true,
},
})
webpack.config.js — Remote (catalog)
// apps/catalog/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container
const deps = require('./package.json').dependencies
module.exports = (env, argv) => ({
mode: argv.mode ?? 'development',
entry: './src/index.ts',
output: {
publicPath: 'auto',
filename: '[name].[contenthash].js',
clean: true,
},
plugins: [
new ModuleFederationPlugin({
name: 'catalog',
filename: 'remoteEntry.js', // точка входу для host
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
'./useCart': './src/hooks/useCart',
},
shared: {
react: {
singleton: true,
requiredVersion: deps.react,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
},
'react-router-dom': {
singleton: true,
},
},
}),
],
devServer: {
port: 3001,
// CORS — host на іншому порту
headers: { 'Access-Control-Allow-Origin': '*' },
historyApiFallback: true,
},
})
Bootstrap-паттерн
Module Federation потребує асинхронного завантаження. Точка входу має бути асинхронною:
// src/index.ts (в КОЖНОМУ додатку)
import('./bootstrap')
// src/bootstrap.tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
const root = createRoot(document.getElementById('root')!)
root.render(<App />)
Без цього отримаємо Shared module is not available for eager consumption.
Використання remote у host
// apps/shell/src/App.tsx
import React, { Suspense, lazy } from 'react'
import { Routes, Route } from 'react-router-dom'
// TypeScript не знає про remote-модулі без оголошень
const ProductList = lazy(() => import('catalog/ProductList'))
const ProductDetail = lazy(() => import('catalog/ProductDetail'))
const Checkout = lazy(() => import('checkout/CheckoutFlow'))
function App() {
return (
<Routes>
<Route
path="/products"
element={
<Suspense fallback={<PageSkeleton />}>
<ProductList />
</Suspense>
}
/>
<Route
path="/products/:id"
element={
<Suspense fallback={<PageSkeleton />}>
<ProductDetail />
</Suspense>
}
/>
<Route
path="/checkout"
element={
<Suspense fallback={<PageSkeleton />}>
<Checkout />
</Suspense>
}
/>
</Routes>
)
}
TypeScript-оголошення для remote-модулів
npm install @module-federation/typescript
// webpack.config.js (remote)
const { FederatedTypesPlugin } = require('@module-federation/typescript')
plugins: [
new ModuleFederationPlugin({ ... }),
new FederatedTypesPlugin({
federationConfig: {
name: 'catalog',
exposes: { './ProductList': './src/components/ProductList' },
},
}),
]
// webpack.config.js (host)
plugins: [
new ModuleFederationPlugin({ ... }),
new FederatedTypesPlugin({
federationConfig: {
name: 'shell',
remotes: { catalog: 'catalog@...' },
},
}),
]
Типи генеруються автоматично та доступні як @mf-types/catalog/ProductList.
Обробка помилок завантаження remote
// components/RemoteComponent.tsx
import React, { Suspense, Component, ReactNode } from 'react'
interface ErrorBoundaryState { hasError: boolean; error?: Error }
class RemoteErrorBoundary extends Component<
{ fallback: ReactNode; children: ReactNode },
ErrorBoundaryState
> {
state: ErrorBoundaryState = { hasError: false }
static getDerivedStateFromError(error: Error) {
return { hasError: true, error }
}
render() {
if (this.state.hasError) return this.props.fallback
return this.props.children
}
}
export function RemoteComponent({
component: Component,
fallback,
errorFallback,
...props
}: {
component: React.ComponentType<unknown>
fallback: ReactNode
errorFallback: ReactNode
[key: string]: unknown
}) {
return (
<RemoteErrorBoundary fallback={errorFallback}>
<Suspense fallback={fallback}>
<Component {...props} />
</Suspense>
</RemoteErrorBoundary>
)
}
Обмін даними між мікрофронтендами
Remote-модулі ізольовані — немає спільного Redux/Zustand. Паттерни для комунікації:
Custom Events:
// catalog remote — публікує подію
window.dispatchEvent(new CustomEvent('catalog:add-to-cart', {
detail: { productId, quantity }
}))
// checkout remote — слухає
window.addEventListener('catalog:add-to-cart', (e: CustomEvent) => {
checkoutStore.addItem(e.detail)
})
Спільний стан через shared-модуль:
// експонуємо стор з auth remote
exposes: {
'./store': './src/store/authStore',
}
// shared: singleton, щоб всі remotes отримали один екземпляр
shared: {
'./src/store/authStore': { singleton: true }
}
CI/CD — незалежне розгортання
# .github/workflows/catalog.yml
name: Deploy Catalog
on:
push:
branches: [main]
paths: ['apps/catalog/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cd apps/catalog && npm ci && npm run build
- name: Deploy to CDN
run: aws s3 sync apps/catalog/dist s3://catalog.example.com --delete
- name: Invalidate CloudFront
run: aws cloudfront create-invalidation --distribution-id $CF_ID --paths "/*"
Host отримує оновлений remote без власного розгортання — при наступному завантаженні сторінки.
Що ми робимо
Проектуємо границі між мікрофронтендами, налаштовуємо webpack з ModuleFederationPlugin в кожному додатку, вирішуємо питання shared-залежностей (React singleton, design system), налаштовуємо TypeScript-оголошення, реалізуємо обробку помилок завантаження remote, вистроюємо CI/CD під незалежне розгортання кожного remote.
Строк: 5–10 днів залежно від кількості remotes та наявності монорепозиторію.







