公司舊系統是 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
// 執行時動態載入,不是建置時的靜態 import
// productModule 是 Host 設定裡定義的 remote 名稱
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
// vite.config.js — Vue 商品模組
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 } // 確保整個頁面只有一個 Vue runtime
}
})
],
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
// vite.config.js — React 主應用程式
import federation from '@originjs/vite-plugin-federation';

export default {
plugins: [
federation({
name: 'main-app',
remotes: {
// build/preview 模式:remoteEntry.js 在 /assets/ 路徑下
// 若對方是 vite dev 模式(需要 build --watch),路徑則是根目錄
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);
}
});

// 清理函式:組件 unmount 時必須呼叫 app.unmount()
// 少了這行,每次 React re-render 或路由切換都會殘留 Vue 實例,記憶體持續洩漏
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
// Vue 組件發送
const handleProductClick = (product) => {
window.dispatchEvent(new CustomEvent('product-selected', {
detail: { product }
}));
};

// React 組件接收
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 }),
}));

// Vue 組件裡用 useSharedStore().setSelectedProduct(...)
// React 組件裡直接 useSharedStore()

效能優化

依賴共享設定

1
2
3
4
5
6
7
8
9
10
shared: {
'react': {
singleton: true,
requiredVersion: '^18.0.0'
},
'lodash': {
singleton: false
// shareScope 不需要明確寫,預設就是 'default'
}
}

shared 的選項欄位只有 singletonrequiredVersionstrictVersioneager 等。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
// 錯誤:ProductList 是 Vue 組件,不能用 React.FC 宣告
// declare module 'productModule/ProductList' {
// import type { FC } from 'react';
// const ProductList: FC<ProductListProps>;
// export default ProductList;
// }

// 正確:用 Vue 的 DefineComponent
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 這個官方維護的替代方案。