微前端實戰:用 Vite Module Federation 打造可擴展的工程管理系統
當你的前端應用越長越大,團隊人數也逐漸增加時,你是否曾經遇過這些問題?
- 每次部署都要重新打包整個應用,即使只改了一個小功能
- 不同功能模組之間耦合嚴重,改 A 壞 B 的情況時有發生
- 多個團隊同時開發時,合併程式碼像是在拆炸彈
如果你點頭如搗蒜,那麼 微前端(Micro-Frontend) 架構可能就是你正在尋找的解決方案。
什麼是微前端?
微前端是將微服務(Microservices)的概念延伸到前端開發的一種架構模式。它允許我們將一個龐大的前端應用拆分成多個 獨立開發、獨立部署、獨立運行 的小型應用,這些小應用可以在運行時動態組合成一個完整的使用者體驗。
Module Federation:微前端的利器
Module Federation
是 Webpack 5 引入的革命性功能,現在透過
@module-federation/vite
套件,我們也能在 Vite 專案中享受這項強大的能力。
Module Federation 就像是一個「分散式的 require()
」,它允許多個獨立的應用程式在
運行時
動態載入並共享程式碼。這意味著:
- 獨立部署 :每個模組可以單獨打包和部署
- 動態載入 :只在需要時才載入模組,優化效能
- 共享依賴 :多個應用可以共享相同的依賴套件,避免重複載入
實戰範例:工程管理系統
讓我們以一個典型的
工程管理系統
為例,看看如何使用
@module-federation/vite
來建構微前端架構。
系統架構概覽
我們的工程管理系統包含以下功能模組:
| 應用 | 角色 | Port | 職責 |
|---|---|---|---|
| Shell App | Host | 3000 | 主框架、路由管理、全域狀態 |
| Dashboard | Remote | 3001 | 儀表板、數據概覽 |
| Project Management | Remote | 3002 | 專案 CRUD、進度追蹤 |
| Resource Management | Remote | 3003 | 人員配置、設備管理 |
| Report Center | Remote | 3004 | 報表生成、數據匯出 |
Step 1:建立專案結構
首先,建立專案的目錄結構:
mkdir engineering-management-systemcd engineering-management-system# 建立各個應用npm create vite@latest shell-app -- --template react-tsnpm create vite@latest dashboard -- --template react-tsnpm create vite@latest project-mgmt -- --template react-tsnpm create vite@latest resource-mgmt -- --template react-tsnpm create vite@latest report-center -- --template react-tsStep 2:安裝 Module Federation 套件
在每個應用中安裝必要的套件:
cd shell-appnpm install @module-federation/viteStep 3:配置 Remote 應用
以 Dashboard 為例,配置 vite.config.ts
:
// dashboard/vite.config.tsimport { 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.tsximport 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 DashboardWidgetStep 4:配置 Host 應用
Shell App 作為主應用,負責載入和組合所有 Remote 應用:
// shell-app/vite.config.tsimport { 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.tsximport 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 AppStep 6:TypeScript 型別宣告
為了讓 TypeScript 正確識別遠端模組,需要新增型別宣告:
// shell-app/src/types/remotes.d.tsdeclare 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.tsexport 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}// 使用 Hookexport 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}部署策略
微前端的一大優勢是可以獨立部署各個模組。以下是常見的部署架構:
在生產環境中,記得更新 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', // ... },})微前端的優缺點
✅ 優點
- 獨立開發部署 :各團隊可以獨立工作,不必等待其他團隊
- 技術棧自由 :不同模組可以使用不同的框架(React、Vue、Svelte...)
- 漸進式升級 :可以逐步升級各模組,降低大規模重構風險
- 更好的可維護性 :程式碼分離清晰,職責明確
- 按需載入 :只載入使用者需要的功能模組
⚠️ 缺點
- 複雜度增加 :需要處理模組間通訊、共享依賴等問題
- 效能考量 :多個遠端載入可能影響首次載入效能
- 開發體驗 :本地開發需要同時啟動多個服務
- 版本管理 :共享依賴的版本衝突需要謹慎處理
總結
微前端架構並非萬能藥,它最適合以下場景:
- 大型企業級應用
- 多團隊協作開發
- 需要逐步現代化的遺留系統
- 功能模組間相對獨立
對於一般的工程管理系統而言,使用 @module-federation/vite
可以有效地將系統拆分成:
- Shell App :處理認證、路由、全域狀態
- Dashboard :提供即時數據概覽
- Project Management :管理專案生命週期
- Resource Management :配置人力與設備
- Report Center :生成各類統計報表
這樣的架構讓各功能模組可以獨立演進,同時又能透過 Module Federation 的共享機制保持一致的使用者體驗。
希望這篇文章能幫助你對微前端架構有更深入的理解。如果你正在考慮將現有的前端應用拆分成微前端,不妨從一個小模組開始試驗,逐步累積經驗後再擴大規模!