📋 目錄
當一個前端 codebase 超過 50 萬行、超過 20 人同時在上面開發,任何改動都像在高樓外牆走鋼索。Micro-Frontends(微前端)把後端微服務的成功經驗帶到前端,讓大型前端得以用「自治團隊 + 獨立部署」的方式運作,代價是增加了相當的複雜度。這篇整理三大主流實作方案的比較與遷移策略。
什麼時候需要微前端?
微前端不是銀彈。在你開始考慮它之前,先問自己三個問題:
- 你的前端 codebase 超過 30 萬行嗎?
- 你有超過 5 個團隊在同一個前端上開發嗎?
- 你的部署頻率受到前端 monolithic 的影響嗎?
如果三個問題都沒有肯定的答案,你很可能不需要微前端。以下才是微前端真正有價值的訊號:
- 不同團隊想要使用不同的技術版本:A 團隊想用 React 18,B 團隊想用 Vue 4,monolithic 的 package.json 無法調和
- 某個子系統需要獨立部署:行銷頁面改版不需要等整個應用上線
- 某個子系統需要單獨回滾:結帳模組出問題,不需要整站回滾
- 程式碼所有權模糊:沒有人敢動某個老舊模組,變成技術債黑洞
三大實作方案
方案一:Module Federation(Webpack 5+)
定位:讓多個獨立 build 輸出共享同一個 runtime,解決了過去 Webpack 時代微前端最大的痛點——重複的 vendor bundle。
Module Federation 是 Webpack 5 引入的功能,核心理念是:每個團隊 build 自己的 bundle,但共享一份 runtime。
// Host 應用(主應用)
// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
// 遠程模組:格式為 "名稱@URL到資產清單"
dashboard: 'dashboard@http://localhost:3001/remoteEntry.js',
checkout: 'checkout@http://localhost:3002/remoteEntry.js',
},
shared: ['react', 'react-dom'], // 共享的依賴,避免重複
}),
],
};
// Remote 應用(dashboard 團隊的子應用)
// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'dashboard',
filename: 'remoteEntry.js', // 暴露給 host 使用的清單
exposes: {
'./Dashboard': './src/Dashboard',
'./Analytics': './src/Analytics',
},
shared: ['react', 'react-dom'],
}),
],
};
// Host 應用中使用 Remote 元件
import React, { Suspense } from 'react';
const Dashboard = React.lazy(() => import('dashboard/Dashboard'));
const Analytics = React.lazy(() => import('dashboard/Analytics'));
function App() {
return (
<div>
<h1>主應用 Header</h1>
<Suspense fallback={<div>載入中...</div>}>
<Dashboard />
</Suspense>
</div>
);
}
Module Federation 的優點:
- Webpack 5 原生支援,設定相對簡單
- 共享 runtime 和 vendor bundle,載入效能比傳統 iframe 方案好
- React、Vue、Svelte 元件都可以暴露
Module Federation 的缺點:
- 理論上 framework-agnostic,實際上 React 的生態系支援最完整
- 共享的依賴版本必須相容(React 18 vs React 17 可能踩坑)
- Debug 複雜——問題可能出在 Host、Remote 或兩者的介面定義上
方案二:Single-SPA
定位:一個「路由級」的微前端框架,讓多個子應用在同一個 DOM 樹上共存,URL 決定哪個子應用被激活。
Single-SPA 的模型是:每個子應用都是一個完整的 SPA,退出和激活由框架管理。
// 主應用的 index.html
<script type="system-importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js",
"vue": "https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js",
"app1": "http://localhost:8081/app1.js",
"app2": "http://localhost:8082/app2.js"
}
}
</script>
// 主應用(root-config.js)
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@org/app1', // 子應用名稱
app: () => System.import('app1'), // 載入方式
activeWhen: ['/app1'], // URL 前綴匹配時激活
customProps: {
// 傳給子應用的全域 props
authToken: 'xxx',
},
});
registerApplication({
name: '@org/app2',
app: () => System.import('app2'),
activeWhen: ['/app2'],
});
start();
// 子應用(app1)的寫法
import { mount, unmount } from '@org/app1/single-spa'; // 或框架adaptor
export function bootstrap(props) {
// 初始化,只跑一次
return Promise.resolve();
}
export function mount(props) {
// 渲染到 DOM
document.getElementById('app1-root').innerHTML = '<h1>App 1 Mounted!</h1>';
return Promise.resolve();
}
export function unmount(props) {
// 清理,組件卸載
return Promise.resolve();
}
Single-SPA 的優點:
- 完全 framework-agnostic——React、Vue、Angular、Preact、Svelte 都可以用
- 概念簡單:每個子應用只需要 export 五個 lifecycle 函數
- URL routing 由框架統一管理
Single-SPA 的缺點:
- 子應用之間共享狀態需要另外處理(Event Bus、Redux shared store 等)
- 每個子應用都需要獨立的 build 配置(沒有 Module Federation 的 shared runtime)
- CSS 是最大的痛點:全域樣式衝突需要嚴格的 naming convention 或 CSS-in-JS
方案三:Web Components(Custom Elements)
定位:利用瀏覽器原生標準,讓任何框架的元件封裝成 Custom Elements,天然跨框架。
<!-- 主應用:直接使用 Custom Elements,無需知道內部用什麼框架 -->
<product-card name="TypeScript 6.0" price="300"></product-card>
// 子應用:封裝成 Web Component
// 任何框架都可以這麼做(React / Vue / Svelte 都支援輸出 Custom Elements)
import { defineCustomElement } from 'vue';
import ProductCard from './ProductCard.vue';
// Vue 3 的 Custom Elements 適配
defineCustomElement(ProductCard, {
// 定義 props 映射
props: {
name: String,
price: Number,
},
}).define('product-card');
// 另一個用 React 寫的同功能元件,同樣的 HTML 使用方式
class ProductCardReact extends HTMLElement {
constructor() {
super();
this._root = this.attachShadow({ mode: 'closed' });
}
static get observedAttributes() {
return ['name', 'price'];
}
connectedCallback() {
this._render();
}
_render() {
this._root.innerHTML = `
<style>
.card { border: 1px solid #e2e8f0; padding: 1rem; border-radius: 8px; }
.name { font-weight: bold; }
.price { color: #059669; }
</style>
<div class="card">
<div class="name">${this.getAttribute('name')}</div>
<div class="price">NT$ ${this.getAttribute('price')}</div>
</div>
`;
}
}
customElements.define('product-card', ProductCardReact);
Web Components 的優點:
- 真正意義上的跨框架——任何地方都能用
- 瀏覽器原生支援,無需 runtime
- Shadow DOM 提供完美的樣式封裝
Web Components 的缺點:
- Styling 隔離在 Shadow DOM 裡,與外部 CSS 的整合(特別是 design token)比較麻煩
- Server-Side Rendering(SSR)支援仍是痛點
- Framework 對 Custom Elements 的支援程度不一,Angular 的支援尤其複雜
三種方案横向比較
| 維度 | Module Federation | Single-SPA | Web Components |
|---|---|---|---|
| Runtime 成本 | 低(共享 runtime) | 中等(各子應有自己的 bundle) | 極低(瀏覽器原生) |
| Framework 支援 | React 最優,Vue/Svelte 可行 | 全部 | 全部 |
| 樣式隔離 | 需要 CSS Modules / naming | 需要命名規範或 CSS-in-JS | Shadow DOM 天然隔離 |
| SSR 支援 | 需要額外設定 | 困難 | 非常困難 |
| 學習曲線 | 中等 | 中等 | 高 |
| 團隊自主性 | 高 | 高 | 最高 |
| 適合規模 | 中大型(多框架混合) | 中型(單框架過渡) | 大型(跨組織分享) |
遷移策略:如何從 Monolithic 走向微前端
遷移微前端不是一次重寫,而是一個漸進的過程。以下是實務上最常成功的策略。
策略一:New == Micro(新增就用微前端)
最保守的起點:新做的功能,直接用微前端架構,老系統維持現狀,兩者通過 iframe 或 REST API 整合。
好處:
- 不動老系統,零風險
- 團隊可以先熟悉微前端的開發流程
- 逐漸建立基礎設施(子應用模板、部署流程、CICD)
<!-- 老系統:index.html -->
<iframe src="http://localhost:3001" style="border: none; width: 100%; height: 600px;"></iframe>
缺點:
- iframe 的效能差(重複下載 vendor、无法共享狀態)
- 只適合完全獨立的子系統
策略二:漸進式拆解(Strangler Fig Pattern)
這是實務上最被推薦的做法,用 MVC 時代 Strangler Fig 應用程式的手法:
Step 1:在 Monolithic 裡找出邊界最清晰的子系統
Step 2:用 Module Federation / Single-SPA 把它抽出來,作為 Remote/子應用
Step 3:舊系統透過介面載入 Remote,驗證功能完整
Step 4:老系統中對應的程式碼標記為 Deprecated(不刪除)
Step 5:等 Remote 穩定,確認沒有問題,再刪除老程式碼
Step 6:重複 Step 1-5
// 過渡期的判斷:本地還是 Remote?
// 有兩種做法:
// 做法 A:Feature Flag
const useMicro = featureFlags.enableMicroFrontends;
if (useMicro) {
const RemoteDashboard = React.lazy(() => import('dashboard/Dashboard'));
} else {
// 舊的 monolithic import
import Dashboard from './monolithic/Dashboard';
}
// 做法 B:URL 切換(推薦)
// NGINX 設定:特定路徑的流量打到子應用
location /dashboard/ {
proxy_pass http://localhost:3001/;
}
策略三:建立微前端底層平台
當你的團隊有 3 個以上的微前端子系統,就應該建立一個共享的「Shell Template」:
# 團隊共享的微前端起始模板
npx create-mf-app@latest my-team-app --template react-ts
這個 Template 應該包含:
- 統一的 Module Federation / Single-SPA 設定
- 共用的 UI 設計系統(如你們有自己的 component library)
- 共享的 auth 邏輯
- 統一的 Error Boundary 處理
- 共享的 logging / monitoring 接入
管理上的挑戰:技術之外的事
遷移微前端最大的挑戰,往往不在技術,而是組織和人。
誰擁有哪個子應用?
每個子應用需要一個明確的「擁有團隊」。如果一個子應用沒有人願意負責,它很快就會變成另一個老舊 monolith。
## 子應用所有權(範例)
| 子應用 | 擁有團隊 | 部署頻率 | SLO |
|--------|----------|----------|-----|
| @org/shell | Platform | 隨時 | 99.9% uptime |
| @org/dashboard | Data Team | 每週 | 99% uptime |
| @org/checkout | Commerce | 每日 | 99.9% uptime |
| @org/marketing | Growth | 隨時 | 99% uptime |
共享依賴的版本策略
Module Federation 最常見的坑:共享依賴版本不一致。當 Remote 用 React 18、Host 用 React 17,兩個 React runtime 之間的狀態管理會出現不可預期的行為。
解法:建立 Company-wide 的 sharedDependencies.json,強制所有子應用使用相同的版本:
{
"shared": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zustand": "^4.4.0",
"date-fns": "^3.0.0"
}
}
UI/UX 一致性
每個團隊自己做的元件,最後會造成同一個應用裡有多套按鈕、不同的 spacing、不同的 colors。沒有 Design System 的團隊,拆成微前端後的使用者體驗會大幅下降。
在遷移之前,先確保你們有一個共用的 Design Token 系統(CSS Variables、Style Dictionary),所有子應用都從這裡取用樣式。
總結:何時選哪個方案
選 Module Federation:
- 你是 Webpack 5 專案(大多數 React 專案都是)
- 團隊想要最大程度的技術自主性
- 你有多個框架的子系統需要共存
- 你願意投資在共享依賴版本管理
選 Single-SPA:
- 你正在從一個大 monolithic SPA 遷移出來
- 你想要一個單純的路由級解決方案
- 你的團隊規模在 3-10 個子應用左右
選 Web Components:
- 你在多個不同組織之間共享元件
- 你的團隊完全沒有統一的技術棧
- 你需要最強的樣式隔離
不要選微前端:
- 你的前端 code < 30 萬行
- 你的團隊 < 5 個人
- 你沒有辦法建立跨團隊的 Design System
- 你的組織還沒有成熟的 CI/CD 和自動化測試
延伸閱讀
- 2026 前端框架比較:React、Vue、Svelte 誰與爭鋒 — 框架選型時的架構考量
- React Server Components 完整指南 — 元件級的服務端渲染策略
微前端是一個組織複雜度管理工具,而不是技術複雜度解決方案。當你的組織準備好承受微前端帶來的額外複雜度時,它會是一個強大的架構選擇。