公司舊系統是 Vue 2,新功能想用 React,遷移又不能一刀切——這個局面在中大型前端團隊裡比想像中常見。Module Federation 最初是 Webpack 5 的功能,後來 @originjs/vite-plugin-federation 把這套機制帶進 Vite 生態,讓跨框架整合在開發體驗上變得可接受。
這篇文章拆解它的原理、給出可實際跑起來的設定,並點出幾個抄網路範例會踩到的坑。
微前端在解決什麼問題
一個大型電商平台,商品展示、購物車、會員中心分屬不同團隊。傳統做法是把所有功能打包成單一應用程式,結果:
- 購物車改一行,整站要重新部署
- 商品團隊想升 Vue 3,但購物車還繫在 Vue 2 的 API 上
- 測試牽一髮動全身,沒人敢在上線前改超過三個檔案
微前端把這個大應用拆成多個獨立部署的模組,每個模組有自己的建置流程、自己的技術棧,只在瀏覽器端組合成完整的畫面。
Module Federation 的核心機制
Module Federation 讓不同應用程式在執行時動態分享程式碼,不是在建置時打包在一起。
架構裡有兩個角色:
Host(主應用程式):載入並整合遠端模組的容器,也是使用者最終看到的完整應用程式。
Remote(遠端應用程式):獨立開發、部署的模組,把自己的組件或功能「暴露」給其他應用程式使用。
為什麼 React 能整合 Vue
關鍵在於 React 和 Vue 最終都在操作 DOM。Module Federation 在瀏覽器執行時動態把 Vue 組件的 JS 載進來,Vue runtime 在指定的 DOM 節點內獨立初始化、跑自己的生命週期,和 React 的組件樹完全隔開。
1 2 3
|
const VueComponent = await import('productModule/ProductList');
|
只要兩個框架各自操作不同的 DOM 節點,它們就不會互相干擾。
實戰設定:電商平台範例
Remote 應用程式(Vue 商品模組)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import federation from '@originjs/vite-plugin-federation';
export default { plugins: [ federation({ name: 'product-module', filename: 'remoteEntry.js', exposes: { './ProductList': './src/components/ProductList.vue', './ProductDetail': './src/components/ProductDetail.vue' }, shared: { 'vue': { singleton: true } } }) ], server: { port: 5001 } }
|
注意:Vue 這裡要設 singleton: true,不是 false。Remote 和 Host 若各自跑一個獨立的 Vue runtime,會造成 provide/inject、全域插件(Pinia、Router)失效,在跨框架整合場景下是很常見的坑。
Host 應用程式(React 主應用程式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import federation from '@originjs/vite-plugin-federation';
export default { plugins: [ federation({ name: 'main-app', remotes: { productModule: "http://localhost:5001/assets/remoteEntry.js", }, shared: { 'react': { singleton: true }, 'react-dom': { singleton: true } } }) ] }
|
路徑說明:@originjs/vite-plugin-federation 的 Remote 端不支援純 dev 模式(Vite 的 ESM bundleless 和 Module Federation 的執行時分享機制不相容),Remote 必須先跑 vite build --watch 才能被 Host 引用。build 產出的 remoteEntry.js 會放在 dist/assets/ 下,所以 URL 要帶 /assets/ 前綴。
在 React 中掛載 Vue 組件
這段是整個整合流程最容易出 bug 的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import React, { useEffect, useRef } from 'react';
const ProductSection: React.FC = () => { const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => { let app: ReturnType<typeof import('vue')['createApp']> | null = null;
import('productModule/ProductList').then(async (module) => { const ProductList = module.default; if (containerRef.current) { const { createApp } = await import('vue'); app = createApp(ProductList); app.mount(containerRef.current); } });
return () => { if (app) { app.unmount(); app = null; } }; }, []);
return ( <div className="product-section"> <h1>我們的商品(React 標題)</h1> <div ref={containerRef} /> {/* Vue 組件會在這裡渲染 */} </div> ); };
|
跨框架通訊
自訂事件
最輕量的方式,適合低頻率的事件通知:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const handleProductClick = (product) => { window.dispatchEvent(new CustomEvent('product-selected', { detail: { product } })); };
useEffect(() => { const handleSelection = (event: CustomEvent) => { console.log('選了:', event.detail.product); };
window.addEventListener('product-selected', handleSelection as EventListener); return () => window.removeEventListener('product-selected', handleSelection as EventListener); }, []);
|
共享狀態管理
Zustand 是框架無關的 store,React 和 Vue 都可以直接引用:
1 2 3 4 5 6 7 8 9
| import { create } from 'zustand';
const useSharedStore = create((set) => ({ selectedProduct: null, setSelectedProduct: (product) => set({ selectedProduct: product }), }));
|
效能優化
依賴共享設定
1 2 3 4 5 6 7 8 9 10
| shared: { 'react': { singleton: true, requiredVersion: '^18.0.0' }, 'lodash': { singleton: false } }
|
shared 的選項欄位只有 singleton、requiredVersion、strictVersion、eager 等。modulePreload 不是 shared 的有效欄位,那是 Vite build 本身的設定,放在 shared 物件裡會被靜默忽略。
懶載入遠端組件
1 2 3 4 5
| const LazyVueComponent = React.lazy(() => import('productModule/ProductList').then(module => ({ default: () => <VueWrapper component={module.default} /> })) );
|
預載入關鍵模組
預載入要在應用程式啟動時觸發,不是等用戶操作才載:
1 2 3 4 5 6 7 8
| const preloadModules = async () => { await Promise.all([ import('productModule/ProductList'), import('cartModule/ShoppingCart') ]); };
preloadModules();
|
TypeScript 型別宣告
Vue 組件的型別要用 Vue 的型別,不是 React 的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
import type { DefineComponent } from 'vue';
interface ProductListProps { category?: string; }
declare module 'productModule/ProductList' { const ProductList: DefineComponent<ProductListProps>; export default ProductList; }
|
載入狀態與錯誤處理
追蹤遠端模組狀態
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const useRemoteComponent = (modulePath: string) => { const [component, setComponent] = useState<any>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null);
useEffect(() => { import(modulePath) .then(module => { setComponent(() => module.default); setLoading(false); }) .catch(err => { setError(err); setLoading(false); }); }, [modulePath]);
return { component, loading, error }; };
|
Error Boundary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class RemoteComponentErrorBoundary extends React.Component< { children: React.ReactNode }, { hasError: boolean } > { constructor(props: { children: React.ReactNode }) { super(props); this.state = { hasError: false }; }
static getDerivedStateFromError() { return { hasError: true }; }
render() { if (this.state.hasError) { return <div>遠端組件載入失敗,請稍後再試</div>; } return this.props.children; } }
|
開發環境 Fallback
1 2 3 4 5 6 7 8 9 10 11
| const loadRemoteComponent = async (modulePath, fallback) => { try { return await import(modulePath); } catch (error) { if (process.env.NODE_ENV === 'development') { console.warn(`無法載入遠端模組 ${modulePath},改用 fallback`); return fallback; } throw error; } };
|
什麼時候值得用,什麼時候不用
Module Federation 適合幾個具體場景:多個團隊同時開發、需要讓不同模組獨立部署更新、或是漸進式把 Vue 2 遷移到 React 而不想一次重寫全部。
以下情況不適合:
- 小型專案或單一團隊——複雜度和收益不成比例
- 對初始載入時間很敏感的應用——多框架 runtime 的載入開銷是真實存在的,即使有 shared 設定也省不掉框架本身的體積
- 全團隊同一個技術棧——沒有理由引入跨框架整合的複雜性
漸進式遷移是這個技術最有說服力的使用場景:在不停機、不大規模重寫的前提下,讓新功能用新框架,舊模組繼續跑舊版本,等業務允許再逐步替換。
@originjs/vite-plugin-federation 是目前最成熟的 Vite Module Federation 方案,但要注意它目前對 Remote 的 dev 模式有限制(需要 vite build --watch)。若需要更完整的 dev 模式支援,可以評估 module-federation/vite 這個官方維護的替代方案。