マイクロフロントエンドとは
マイクロフロントエンドは、マイクロサービスの考え方をフロントエンドに適用したアーキテクチャパターンです。大規模なアプリケーションを、独立してデプロイ可能な小さなアプリケーションに分割します。
flowchart TB
subgraph Monolithic["モノリシックフロントエンド"]
subgraph Single["Single Application"]
M1["Product<br/>Module"]
M2["Cart<br/>Module"]
M3["Account<br/>Module"]
end
Note1["単一のデプロイ、単一のチーム"]
end
subgraph MicroFE["マイクロフロントエンド"]
subgraph Shell["Container App (Shell)"]
Header["Shared Header"]
subgraph Apps["独立アプリケーション"]
A1["Product App<br/>(Team A)"]
A2["Cart App<br/>(Team B)"]
A3["Account App<br/>(Team C)"]
end
end
Note2["独立デプロイ、独立チーム、技術選択の自由"]
end
導入を検討すべきケース
| ケース | 理由 |
|---|
| 大規模チーム(10+人) | チーム間の独立性を確保 |
| レガシーシステムの段階的移行 | 新旧技術の共存が可能 |
| 異なるリリースサイクル | 機能ごとに独立してデプロイ |
| ドメイン駆動組織 | ビジネスドメインでチーム分割 |
| 技術スタックの多様性 | React, Vue, Angular の共存 |
統合パターン
1. Module Federation(Webpack 5)
flowchart TB
subgraph Host["Host App (Shell)"]
Config["webpack.config.js<br/>remotes: {<br/> products, cart, account<br/>}"]
end
Host --> Products
Host --> Cart
Host --> Account
subgraph Products["Products Remote"]
P1["exposes:<br/>- List<br/>- Detail"]
end
subgraph Cart["Cart Remote"]
C1["exposes:<br/>- Cart<br/>- Summary"]
end
subgraph Account["Account Remote"]
A1["exposes:<br/>- Profile<br/>- Settings"]
end
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
products: 'products@http://localhost:3001/remoteEntry.js',
cart: 'cart@http://localhost:3002/remoteEntry.js',
account: 'account@http://localhost:3003/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
import React, { Suspense, lazy } from 'react';
const ProductList = lazy(() => import('products/ProductList'));
const Cart = lazy(() => import('cart/Cart'));
const AccountSettings = lazy(() => import('account/Settings'));
function App() {
return (
<div>
<Header />
<Routes>
<Route
path="/products/*"
element={
<Suspense fallback={<Skeleton />}>
<ProductList />
</Suspense>
}
/>
<Route
path="/cart"
element={
<Suspense fallback={<Skeleton />}>
<Cart />
</Suspense>
}
/>
<Route
path="/account/*"
element={
<Suspense fallback={<Skeleton />}>
<AccountSettings />
</Suspense>
}
/>
</Routes>
</div>
);
}
2. Single-SPA
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@myorg/products',
app: () => System.import('@myorg/products'),
activeWhen: ['/products'],
});
registerApplication({
name: '@myorg/cart',
app: () => System.import('@myorg/cart'),
activeWhen: ['/cart'],
});
registerApplication({
name: '@myorg/account',
app: () => System.import('@myorg/account'),
activeWhen: ['/account'],
});
registerApplication({
name: '@myorg/navbar',
app: () => System.import('@myorg/navbar'),
activeWhen: () => true,
});
start();
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './App';
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
errorBoundary(err, info, props) {
return <div>Error in products app</div>;
},
});
export const { bootstrap, mount, unmount } = lifecycles;
3. Native Federation(Vite/Rspack対応)
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
federation({
name: 'host-app',
remotes: {
products: 'http://localhost:3001/assets/remoteEntry.js',
cart: 'http://localhost:3002/assets/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
});
状態管理と通信
flowchart TB
subgraph Events["1. Custom Events (推奨)"]
EA["App A"] -->|CustomEvent| EB["App B"]
end
subgraph SharedState["2. Shared State (複雑な場合)"]
SA["App A"] --> Store["Store<br/>(Global)"]
SC["App C"] --> Store
Store --> SB["App B"]
Store --> SD["App D"]
end
subgraph URL["3. URL/Query Params"]
URLNote["ナビゲーション情報の共有"]
end
Custom Events
export const CartEvents = {
ITEM_ADDED: 'cart:item-added',
ITEM_REMOVED: 'cart:item-removed',
CART_UPDATED: 'cart:updated',
} as const;
export interface CartItemEvent {
productId: string;
quantity: number;
price: number;
}
export function dispatchCartEvent(
type: string,
detail: CartItemEvent
): void {
window.dispatchEvent(new CustomEvent(type, { detail }));
}
export function subscribeToCartEvent(
type: string,
handler: (event: CustomEvent<CartItemEvent>) => void
): () => void {
window.addEventListener(type, handler as EventListener);
return () => window.removeEventListener(type, handler as EventListener);
}
import { CartEvents, dispatchCartEvent } from '@myorg/events';
function ProductCard({ product }) {
const handleAddToCart = () => {
dispatchCartEvent(CartEvents.ITEM_ADDED, {
productId: product.id,
quantity: 1,
price: product.price,
});
};
return (
<div>
<h3>{product.name}</h3>
<button onClick={handleAddToCart}>Add to Cart</button>
</div>
);
}
import { useEffect, useState } from 'react';
import { CartEvents, subscribeToCartEvent } from '@myorg/events';
function Cart() {
const [items, setItems] = useState([]);
useEffect(() => {
const unsubscribe = subscribeToCartEvent(CartEvents.ITEM_ADDED, (event) => {
setItems((prev) => [...prev, event.detail]);
});
return unsubscribe;
}, []);
return (
<div>
<h2>Cart ({items.length} items)</h2>
{}
</div>
);
}
グローバル状態の共有
import { create } from 'zustand';
interface User {
id: string;
name: string;
email: string;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
login: (user: User) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
}));
import { useAuthStore } from '@myorg/shared-store';
function Header() {
const { user, isAuthenticated, logout } = useAuthStore();
return (
<header>
{isAuthenticated ? (
<>
<span>Welcome, {user?.name}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<a href="/login">Login</a>
)}
</header>
);
}
ルーティング戦略
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const ProductsRoutes = lazy(() => import('products/Routes'));
const CartRoutes = lazy(() => import('cart/Routes'));
const AccountRoutes = lazy(() => import('account/Routes'));
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{
path: 'products/*',
element: (
<Suspense fallback={<Loading />}>
<ProductsRoutes />
</Suspense>
),
},
{
path: 'cart/*',
element: (
<Suspense fallback={<Loading />}>
<CartRoutes />
</Suspense>
),
},
{
path: 'account/*',
element: (
<Suspense fallback={<Loading />}>
<AccountRoutes />
</Suspense>
),
},
],
},
]);
export function AppRouter() {
return <RouterProvider router={router} />;
}
デプロイ戦略
flowchart TB
subgraph CDN["CDN構成"]
Shell["shell/<br/>└── v1.2.3/"]
Products["products/<br/>└── v2.1.0/"]
Cart["cart/<br/>└── v1.8.5/"]
Account["account/<br/>└── v3.0.1/"]
end
subgraph Manifest["マニフェスト管理"]
JSON["products: cdn/products/v2.1.0/<br/>cart: cdn/cart/v1.8.5/<br/>account: cdn/account/v3.0.1/"]
end
CDN --> Manifest
Note["Blue-Green / Canary デプロイ対応可能"]
共通UIコンポーネント
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';
export { Modal } from './Modal';
export { theme } from './theme';
{
shared: {
'@myorg/ui-kit': {
singleton: true,
requiredVersion: '^1.0.0',
},
},
}
テスト戦略
| レベル | 対象 | ツール |
|---|
| ユニット | 個別コンポーネント | Vitest, Jest |
| 統合 | マイクロFE単体 | Testing Library |
| E2E | 全体統合 | Playwright, Cypress |
| コントラクト | API契約 | Pact |
参考リンク
← 一覧に戻る