現代化開發者工作空間,展示程式碼編輯器與數位設計工具
Photo by Pexels

微前端實戰:用 Vite Module Federation 打造可擴展的工程管理系統

當你的前端應用越長越大,團隊人數也逐漸增加時,你是否曾經遇過這些問題?

  • 每次部署都要重新打包整個應用,即使只改了一個小功能
  • 不同功能模組之間耦合嚴重,改 A 壞 B 的情況時有發生
  • 多個團隊同時開發時,合併程式碼像是在拆炸彈

如果你點頭如搗蒜,那麼 微前端(Micro-Frontend) 架構可能就是你正在尋找的解決方案。

什麼是微前端?

微前端是將微服務(Microservices)的概念延伸到前端開發的一種架構模式。它允許我們將一個龐大的前端應用拆分成多個 獨立開發、獨立部署、獨立運行 的小型應用,這些小應用可以在運行時動態組合成一個完整的使用者體驗。

Mermaid Diagram
Mermaid Diagram

Module Federation:微前端的利器

程式碼特寫,展示軟體開發中的模組化程式設計
Photo by Pexels

Module Federation 是 Webpack 5 引入的革命性功能,現在透過 @module-federation/vite 套件,我們也能在 Vite 專案中享受這項強大的能力。

Module Federation 就像是一個「分散式的 require() 」,它允許多個獨立的應用程式在 運行時 動態載入並共享程式碼。這意味著:

  1. 獨立部署 :每個模組可以單獨打包和部署
  2. 動態載入 :只在需要時才載入模組,優化效能
  3. 共享依賴 :多個應用可以共享相同的依賴套件,避免重複載入

實戰範例:工程管理系統

開發團隊協作場景,多人使用筆電和平板進行專案開發
Photo by Pexels

讓我們以一個典型的 工程管理系統 為例,看看如何使用 @module-federation/vite 來建構微前端架構。

系統架構概覽

我們的工程管理系統包含以下功能模組:

應用 角色 Port 職責
Shell App Host 3000 主框架、路由管理、全域狀態
Dashboard Remote 3001 儀表板、數據概覽
Project Management Remote 3002 專案 CRUD、進度追蹤
Resource Management Remote 3003 人員配置、設備管理
Report Center Remote 3004 報表生成、數據匯出
Mermaid Diagram

Step 1:建立專案結構

首先,建立專案的目錄結構:

mkdir engineering-management-system
cd engineering-management-system
# 建立各個應用
npm create vite@latest shell-app -- --template react-ts
npm create vite@latest dashboard -- --template react-ts
npm create vite@latest project-mgmt -- --template react-ts
npm create vite@latest resource-mgmt -- --template react-ts
npm create vite@latest report-center -- --template react-ts

Step 2:安裝 Module Federation 套件

在每個應用中安裝必要的套件:

cd shell-app
npm install @module-federation/vite

Step 3:配置 Remote 應用

以 Dashboard 為例,配置 vite.config.ts

// dashboard/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { federation } from '@module-federation/vite'
export default defineConfig({
plugins: [
react(),
federation({
name: 'dashboard',
filename: 'remoteEntry.js',
// 暴露給其他應用使用的模組
exposes: {
'./DashboardWidget': './src/components/DashboardWidget.tsx',
'./ProjectOverview': './src/components/ProjectOverview.tsx',
'./ResourceSummary': './src/components/ResourceSummary.tsx',
},
// 與其他應用共享的依賴
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
server: {
port: 3001,
cors: true,
},
build: {
target: 'esnext',
minify: false,
},
})

Dashboard 的元件範例:

// dashboard/src/components/DashboardWidget.tsx
import React from 'react'
interface ProjectStats {
total: number
inProgress: number
completed: number
delayed: number
}
interface DashboardWidgetProps {
stats: ProjectStats
onProjectClick?: (status: string) => void
}
export const DashboardWidget: React.FC<DashboardWidgetProps> = ({
stats,
onProjectClick
}) => {
const cards = [
{ label: '總專案數', value: stats.total, color: 'bg-blue-500', status: 'all' },
{ label: '進行中', value: stats.inProgress, color: 'bg-green-500', status: 'in-progress' },
{ label: '已完成', value: stats.completed, color: 'bg-gray-500', status: 'completed' },
{ label: '延遲中', value: stats.delayed, color: 'bg-red-500', status: 'delayed' },
]
return (
<div className="grid grid-cols-4 gap-4">
{cards.map((card) => (
<div
key={card.status}
className={`${card.color} rounded-lg p-4 text-white cursor-pointer
hover:opacity-90 transition-opacity`}
onClick={() => onProjectClick?.(card.status)}
>
<p className="text-sm opacity-80">{card.label}</p>
<p className="text-3xl font-bold">{card.value}</p>
</div>
))}
</div>
)
}
export default DashboardWidget

Step 4:配置 Host 應用

Shell App 作為主應用,負責載入和組合所有 Remote 應用:

// shell-app/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { federation } from '@module-federation/vite'
export default defineConfig({
plugins: [
react(),
federation({
name: 'shell',
// 聲明要使用的遠端應用
remotes: {
dashboard: 'dashboard@http://localhost:3001/remoteEntry.js',
projectMgmt: 'projectMgmt@http://localhost:3002/remoteEntry.js',
resourceMgmt: 'resourceMgmt@http://localhost:3003/remoteEntry.js',
reportCenter: 'reportCenter@http://localhost:3004/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
server: {
port: 3000,
},
build: {
target: 'esnext',
},
})

Step 5:在 Host 中使用 Remote 元件

// shell-app/src/App.tsx
import React, { Suspense, lazy } from 'react'
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
// 動態載入遠端元件
const DashboardWidget = lazy(() => import('dashboard/DashboardWidget'))
const ProjectList = lazy(() => import('projectMgmt/ProjectList'))
const ResourceCalendar = lazy(() => import('resourceMgmt/ResourceCalendar'))
const ReportGenerator = lazy(() => import('reportCenter/ReportGenerator'))
// 載入中的 Fallback 元件
const LoadingFallback = () => (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" />
</div>
)
// 錯誤邊界元件
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return this.props.fallback
}
return this.props.children
}
}
const App: React.FC = () => {
return (
<BrowserRouter>
<div className="min-h-screen bg-gray-100">
{/* 導覽列 */}
<nav className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between h-16">
<div className="flex items-center space-x-8">
<span className="text-xl font-bold text-blue-600">
工程管理系統
</span>
<Link to="/" className="text-gray-700 hover:text-blue-600">
儀表板
</Link>
<Link to="/projects" className="text-gray-700 hover:text-blue-600">
專案管理
</Link>
<Link to="/resources" className="text-gray-700 hover:text-blue-600">
資源管理
</Link>
<Link to="/reports" className="text-gray-700 hover:text-blue-600">
報表中心
</Link>
</div>
</div>
</div>
</nav>
{/* 主要內容區 */}
<main className="max-w-7xl mx-auto py-6 px-4">
<ErrorBoundary
fallback={
<div className="text-red-500 p-4">
模組載入失敗,請稍後再試
</div>
}
>
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route
path="/"
element={
<DashboardWidget
stats={{ total: 24, inProgress: 12, completed: 8, delayed: 4 }}
onProjectClick={(status) => console.log('Filter by:', status)}
/>
}
/>
<Route path="/projects/*" element={<ProjectList />} />
<Route path="/resources/*" element={<ResourceCalendar />} />
<Route path="/reports/*" element={<ReportGenerator />} />
</Routes>
</Suspense>
</ErrorBoundary>
</main>
</div>
</BrowserRouter>
)
}
export default App

Step 6:TypeScript 型別宣告

為了讓 TypeScript 正確識別遠端模組,需要新增型別宣告:

// shell-app/src/types/remotes.d.ts
declare module 'dashboard/DashboardWidget' {
import { FC } from 'react'
interface ProjectStats {
total: number
inProgress: number
completed: number
delayed: number
}
interface DashboardWidgetProps {
stats: ProjectStats
onProjectClick?: (status: string) => void
}
const DashboardWidget: FC<DashboardWidgetProps>
export default DashboardWidget
}
declare module 'projectMgmt/ProjectList' {
import { FC } from 'react'
const ProjectList: FC
export default ProjectList
}
declare module 'resourceMgmt/ResourceCalendar' {
import { FC } from 'react'
const ResourceCalendar: FC
export default ResourceCalendar
}
declare module 'reportCenter/ReportGenerator' {
import { FC } from 'react'
const ReportGenerator: FC
export default ReportGenerator
}

進階技巧:模組間通訊

在微前端架構中,各模組之間的通訊是一個重要議題。以下是幾種常見的解決方案:

方法一:透過 Props 傳遞

最直接的方式,適合父子元件間的通訊:

<DashboardWidget
stats={projectStats}
onProjectClick={handleProjectFilter}
/>

方法二:Custom Events

適合跨模組、鬆散耦合的通訊:

// 發送事件
window.dispatchEvent(new CustomEvent('project:updated', {
detail: { projectId: '123', status: 'completed' }
}))
// 監聽事件
useEffect(() => {
const handler = (event: CustomEvent) => {
console.log('Project updated:', event.detail)
}
window.addEventListener('project:updated', handler)
return () => window.removeEventListener('project:updated', handler)
}, [])

方法三:共享狀態管理

使用 localStorage 搭配事件:

// utils/sharedState.ts
export const setSharedState = <T>(key: string, value: T) => {
localStorage.setItem(key, JSON.stringify(value))
window.dispatchEvent(new CustomEvent('shared-state-change', {
detail: { key, value }
}))
}
export const getSharedState = <T>(key: string): T | null => {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : null
}
// 使用 Hook
export const useSharedState = <T>(key: string, initialValue: T) => {
const [value, setValue] = useState<T>(() =>
getSharedState<T>(key) ?? initialValue
)
useEffect(() => {
const handler = (event: CustomEvent) => {
if (event.detail.key === key) {
setValue(event.detail.value)
}
}
window.addEventListener('shared-state-change', handler)
return () => window.removeEventListener('shared-state-change', handler)
}, [key])
const updateValue = (newValue: T) => {
setSharedState(key, newValue)
setValue(newValue)
}
return [value, updateValue] as const
}

部署策略

微前端的一大優勢是可以獨立部署各個模組。以下是常見的部署架構:

Mermaid Diagram

在生產環境中,記得更新 Remote URL:

// shell-app/vite.config.ts (production)
federation({
name: 'shell',
remotes: {
dashboard: 'dashboard@https://dashboard.example.com/remoteEntry.js',
projectMgmt: 'projectMgmt@https://project.example.com/remoteEntry.js',
// ...
},
})

微前端的優缺點

✅ 優點

  1. 獨立開發部署 :各團隊可以獨立工作,不必等待其他團隊
  2. 技術棧自由 :不同模組可以使用不同的框架(React、Vue、Svelte...)
  3. 漸進式升級 :可以逐步升級各模組,降低大規模重構風險
  4. 更好的可維護性 :程式碼分離清晰,職責明確
  5. 按需載入 :只載入使用者需要的功能模組

⚠️ 缺點

  1. 複雜度增加 :需要處理模組間通訊、共享依賴等問題
  2. 效能考量 :多個遠端載入可能影響首次載入效能
  3. 開發體驗 :本地開發需要同時啟動多個服務
  4. 版本管理 :共享依賴的版本衝突需要謹慎處理

總結

微前端架構並非萬能藥,它最適合以下場景:

  • 大型企業級應用
  • 多團隊協作開發
  • 需要逐步現代化的遺留系統
  • 功能模組間相對獨立

對於一般的工程管理系統而言,使用 @module-federation/vite 可以有效地將系統拆分成:

  • Shell App :處理認證、路由、全域狀態
  • Dashboard :提供即時數據概覽
  • Project Management :管理專案生命週期
  • Resource Management :配置人力與設備
  • Report Center :生成各類統計報表

這樣的架構讓各功能模組可以獨立演進,同時又能透過 Module Federation 的共享機制保持一致的使用者體驗。

希望這篇文章能幫助你對微前端架構有更深入的理解。如果你正在考慮將現有的前端應用拆分成微前端,不妨從一個小模組開始試驗,逐步累積經驗後再擴大規模!