Module Federation 入門
深入理解 Module Federation 的 Host / Remote / Container 概念,以及 Webpack 5 的設定與 shared dependencies 機制
Overview##
在上一篇中,我們比較了 Micro-Frontend 的三種整合模式。其中 Module Federation 是效能最好、開發體驗最接近 monolith 的方案。
Module Federation 是 Webpack 5 內建的功能,核心能力是:讓多個獨立 build 的應用在 runtime 動態載入彼此的模組,就像它們原本就在同一個 bundle 裡一樣。
這篇會深入它的運作機制、設定方式,以及最容易踩坑的 shared dependencies 管理。
Architecture##
三個核心角色###
Module Federation 的架構由三個角色組成:
graph TD
H[Host App] -->|loads at runtime| RE1[remoteEntry.js]
H -->|loads at runtime| RE2[remoteEntry.js]
RE1 --> R1[Remote A: Product]
RE2 --> R2[Remote B: Cart]
R1 -->|exposes| M1[ProductCard]
R1 -->|exposes| M2[ProductList]
R2 -->|exposes| M3[CartWidget]
| 角色 | 說明 | 類比 |
|---|---|---|
| Host | 主應用,負責載入其他應用的模組 | 插座(接收插頭) |
| Remote | 被載入的應用,對外暴露特定模組 | 插頭(提供功能) |
| Container | Remote 的入口檔案(remoteEntry.js),提供 get() 和 init() 方法 | 插頭上的規格標籤 |
Note
同一個應用可以同時是 Host 和 Remote。例如,Product App 可以 expose 自己的元件給 Shell,同時 consume Cart App 的購物車 Widget。這讓架構非常靈活。
載入流程###
當 Host 需要一個 Remote 的模組時,實際發生的事情是:
sequenceDiagram
participant H as Host
participant C as Container (remoteEntry.js)
participant R as Remote Chunk
H->>C: 1. Load remoteEntry.js
H->>C: 2. init(sharedScope)
H->>C: 3. get('./ProductCard')
C->>R: 4. Load module chunk
R-->>H: 5. Return module
- Host 載入 Remote 的
remoteEntry.js(Container) - Host 呼叫
init(),傳入 shared scope(共享依賴的版本資訊) - Host 呼叫
get('./ProductCard'),請求特定模組 - Container 非同步載入對應的 chunk
- 模組回傳給 Host 使用
Tip
整個過程是非同步的。這就是為什麼在 React 中使用 Remote 模組時,一定要用 React.lazy() + <Suspense> — 模組是在 runtime 才下載的。
Shared Dependencies 機制###
Module Federation 最強大的功能之一是 dependency sharing — 多個應用共用同一份 React,而不是各自打包一份。
graph TD
subgraph Without Sharing
A1[Host: React 18.2]
B1[Remote A: React 18.2]
C1[Remote B: React 18.2]
end
subgraph With Sharing
S[Shared: React 18.2]
A2[Host] --> S
B2[Remote A] --> S
C2[Remote B] --> S
end
沒有 sharing 時,3 個應用各打包一份 React(~140KB x 3 = 420KB)。有 sharing 時,只載入一份(~140KB)。
但 sharing 不只是省 bundle size — 對 React 來說,singleton 是必要的。如果同時存在兩個 React instance,hooks 會壞掉。
Setup##
Prerequisites###
- Node.js 18+
- Webpack 5 或 Rspack
Remote 端設定(被載入的應用)###
// apps/product/webpack.config.ts
import { readFileSync } from 'node:fs';
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const deps = JSON.parse(
readFileSync(new URL('./package.json', import.meta.url), 'utf-8'),
).dependencies;
export default {
mode: 'production',
output: {
publicPath: 'https://product.example.com/',
},
plugins: [
new ModuleFederationPlugin({
// unique name for this remote
name: 'productApp',
// container entry filename
filename: 'remoteEntry.js',
// modules to expose to consumers
exposes: {
'./ProductCard': './src/components/ProductCard',
'./ProductList': './src/components/ProductList',
},
// shared dependencies
shared: {
react: {
singleton: true,
requiredVersion: deps.react,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
Host 端設定(主應用)###
// apps/shell/webpack.config.ts
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
// declare remotes and their entry points
remotes: {
productApp: 'productApp@https://product.example.com/remoteEntry.js',
cartApp: 'cartApp@https://cart.example.com/remoteEntry.js',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
},
},
}),
],
};
Usage##
Basic Usage###
Remote 端 — 暴露元件:
// apps/product/src/components/ProductCard.tsx
interface ProductCardProps {
productId: string;
onAddToCart?: (id: string) => void;
}
export default function ProductCard({ productId, onAddToCart }: ProductCardProps) {
return (
<div className="product-card">
<h3>Product {productId}</h3>
<button onClick={() => onAddToCart?.(productId)}>Add to Cart</button>
</div>
);
}
Host 端 — 消費 Remote 元件:
// apps/shell/src/App.tsx
import React, { Suspense } from 'react';
// Dynamic import from remote — this is resolved at runtime
const ProductCard = React.lazy(() => import('productApp/ProductCard'));
export default function App() {
const handleAddToCart = (id: string) => {
console.log(`Added product ${id} to cart`);
};
return (
<div>
<h1>My Store</h1>
<Suspense fallback={<div>Loading product...</div>}>
<ProductCard productId="42" onAddToCart={handleAddToCart} />
</Suspense>
</div>
);
}
TypeScript 型別支援:
Host 端不知道 Remote 的型別。需要手動宣告:
// apps/shell/src/types/remotes.d.ts
declare module 'productApp/ProductCard' {
import { ComponentType } from 'react';
interface ProductCardProps {
productId: string;
onAddToCart?: (id: string) => void;
}
const ProductCard: ComponentType<ProductCardProps>;
export default ProductCard;
}
Warning
型別宣告需要手動同步。當 Remote 的 props 改變時,Host 的 .d.ts 不會自動更新。Module Federation 2.0 引入了 dynamic type hinting 來解決這個問題,但 Webpack 5 內建版本仍需要手動管理。
Advanced Usage — Shared Dependencies 深入###
Shared dependencies 的設定看似簡單,但版本衝突是 Module Federation 最常見的問題來源。
// Understanding shared configuration options
new ModuleFederationPlugin({
shared: {
react: {
singleton: true, // only one instance allowed globally
requiredVersion: '^18.0.0', // acceptable version range
strictVersion: true, // throw error if version mismatch (not just warning)
eager: true, // include in initial chunk, not async loaded
version: '18.3.1', // advertise this version to other containers
},
},
});
各選項的影響:
| 選項 | 預設 | 說明 | 何時使用 |
|---|---|---|---|
singleton | false | 全域只允許一個 instance | React、Vue 等有 global state 的 library |
requiredVersion | 從 package.json 推斷 | 可接受的 semver 範圍 | 指定相容版本 |
strictVersion | false | 版本不符時 throw error(否則只 warning) | production 環境防止隱性 bug |
eager | false | 放入 initial chunk 而非 async 載入 | 避免 async boundary 或用於 SSR |
version | 從 package.json 推斷 | 對外宣告的版本號 | 覆寫自動推斷的版本 |
常見的版本衝突場景:
Host: react@18.2.0 (singleton: true, requiredVersion: "^18.0.0")
Remote A: react@18.3.1 (singleton: true, requiredVersion: "^18.0.0")
Remote B: react@17.0.2 (singleton: true, requiredVersion: "^17.0.0")
Host 和 Remote A 相容(都是 18.x),但 Remote B 要求 17.x。Module Federation 的行為:
strictVersion: false(預設) → 使用已載入的 18.x 版本,console 印出 warningstrictVersion: true→ 直接 throw error,應用崩潰
Warning
Singleton 衝突是最危險的問題。 如果 React 同時存在兩個 instance,所有 hooks 都會壞掉(Invalid hook call error)。確保所有 Host 和 Remote 的 React 版本在同一個 major version。
錯誤處理###
Remote 可能因為網路問題或部署時間差而載入失敗。必須做好 fallback:
// apps/shell/src/components/RemoteWrapper.tsx
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
interface RemoteWrapperProps {
children: React.ReactNode;
fallback?: React.ReactNode;
}
function ErrorFallback({ error }: { error: Error }) {
return (
<div role="alert">
<p>This section is temporarily unavailable.</p>
<pre>{error.message}</pre>
</div>
);
}
export function RemoteWrapper({ children, fallback }: RemoteWrapperProps) {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={fallback ?? <div>Loading...</div>}>{children}</Suspense>
</ErrorBoundary>
);
}
// Usage
<RemoteWrapper fallback={<ProductCardSkeleton />}>
<ProductCard productId="42" />
</RemoteWrapper>
Configuration Reference##
ModuleFederationPlugin 主要選項###
| 選項 | 型別 | 說明 |
|---|---|---|
name | string | Container 的唯一名稱,不可重複 |
filename | string | Container entry 的檔名,通常是 remoteEntry.js |
exposes | Record<string, string> | 對外暴露的模組路徑映射 |
remotes | Record<string, string> | 要消費的 Remote 應用列表 |
shared | Array | Record | 共享依賴的設定 |
library | { type, name } | Container 的 library 輸出格式 |
shareScope | string | 共享作用域的名稱,預設 default |
Remote 字串格式###
remotes: {
// format: "<internalName>": "<containerName>@<remoteEntryUrl>"
productApp: "productApp@https://cdn.example.com/product/remoteEntry.js",
}
安裝###
| 指令 | 用途 |
|---|---|
pnpm add webpack@5 -D | 安裝 webpack 5(已內建 Module Federation) |
pnpm add @module-federation/enhanced -D | 安裝 Module Federation 2.0 enhanced 版本 |
Quiz##
Summary##
- Module Federation 讓多個獨立 build 在 runtime 共享模組。 Host 載入 Remote 的
remoteEntry.js,透過init()+get()取得模組。 - 三個核心角色:Host(消費者)、Remote(提供者)、Container(入口)。 同一個應用可以同時扮演 Host 和 Remote。
- Shared dependencies 是最重要也最容易出錯的設定。 React 等 library 務必設為
singleton: true,並確保所有應用的 major version 一致。 - 永遠要做錯誤處理。 Remote 可能載入失敗,用
ErrorBoundary+Suspense確保 Host 不會因此崩潰。 - 型別需要手動同步。 Webpack 5 內建的 Module Federation 沒有自動型別推導,考慮升級到 Module Federation 2.0 或手動維護
.d.ts。
留言 (0)
登入後即可留言