還在為「公司要從 Vue 轉 React」或「新專案用什麼框架」而頭痛嗎?如果我告訴你,現在有一種技術可以讓 React 和 Vue 在同一個專案中完美共存,你會不會覺得我在說夢話?

今天要跟大家分享的 Vite + Module Federation,就是這樣一個讓人眼睛為之一亮的黑科技。它不只解決了框架選擇的痛苦,更開啟了前端架構設計的全新可能性。

什麼是微前端?為什麼我們需要它?

想像一下,你正在開發一個大型電商平台。產品團隊負責商品展示頁面,購物車團隊處理結帳流程,用戶中心團隊維護會員系統。傳統做法是把所有功能打包成一個巨大的應用程式,結果就是:

  • 一個人改了購物車邏輯,整個網站都要重新部署
  • 產品團隊想用最新的 React 18,但購物車團隊還在用 Vue 2
  • 測試時牽一髮動全身,誰都不敢隨便改程式碼

微前端架構就像是把一個大房子分割成多個獨立的套房,每個團隊都有自己的空間,可以自由裝潢、獨立出入,但仍然共享同一個地址。

Module Federation:前端界的樂高積木

Module Federation 最初是 Webpack 5 的旗艦功能,它的核心概念很簡單:讓不同的應用程式可以在執行時動態分享程式碼

這就像是樂高積木一樣,每個團隊負責製作不同的積木塊,最後在瀏覽器中組裝成完整的應用程式。最神奇的是,這些積木塊可以用不同的「材料」製作 —— 有些用 React,有些用 Vue,甚至可以混用!

兩個關鍵角色

在 Module Federation 的世界裡,有兩個重要角色:

Host(主機應用程式):就像是一個容器,負責載入和整合其他應用程式。它是用戶最終看到的完整應用程式。

Remote(遠端應用程式):獨立開發、部署的微前端模組,可以把自己的組件或功能「出租」給其他應用程式使用。

Vite:讓 Module Federation 飛起來

傳統的 Module Federation 需要 Webpack,但 Webpack 的建置速度...你懂的。當專案變大時,喝杯咖啡回來可能都還在編譯。

這時候 Vite 就像是超級英雄一樣出現了!它利用瀏覽器原生的 ES Modules 特性,讓開發體驗快到飛起來。而 @originjs/vite-plugin-federation 這個插件,則是把 Module Federation 的魔法帶到了 Vite 的世界。

為什麼 React 能整合 Vue?技術原理大揭密

這可能是最讓人好奇的問題了。React 和 Vue 不是競爭對手嗎?怎麼可能在同一個應用程式中共存?

1. 執行時載入的魔法

關鍵在於 執行時載入。Module Federation 不是在建置時把所有程式碼打包在一起,而是在瀏覽器執行時動態載入遠端模組。

1
2
// 這不是編譯時的 import,而是執行時的動態載入
const VueComponent = await import('remote-vue-app/VueComponent');

想像一下,你的 React 應用程式已經在瀏覽器中執行了,這時候它說:「嘿,我需要一個 Vue 組件,去遠端抓一下吧!」然後 Vue 組件就被載入進來,在指定的 DOM 節點中開始工作。

2. DOM 的和諧共處

無論是 React 還是 Vue,最終都是在操作 DOM。它們就像是兩個室友,只要各自使用不同的房間(DOM 節點),就不會互相干擾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// React 容器組件
const VueComponentWrapper: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
// 載入 Vue 組件並掛載到指定節點
import('remote-vue-app/VueComponent').then((module) => {
const VueComponent = module.default;
if (containerRef.current) {
const app = createApp(VueComponent);
app.mount(containerRef.current);
}
});
}, []);

return <div ref={containerRef} />; // Vue 的專屬空間
};

3. 框架實例的獨立性

每個框架都有自己的實例和生命週期管理。React 管理自己的組件樹,Vue 在分配給它的 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: false } // 允許多個 Vue 實例
}
})
],
server: {
port: 5001
}
}

Host 應用程式設定(React 主應用程式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vite.config.js (React 主應用程式)
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 }
}
})
]
}

在 React 中使用 Vue 組件

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
// React 主應用程式
import React, { useEffect, useRef } from 'react';

const ProductSection: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
// 動態載入 Vue 商品組件
import('productModule/ProductList').then((module) => {
const ProductList = module.default;
if (containerRef.current) {
const { createApp } = await import('vue');
const app = createApp(ProductList);
app.mount(containerRef.current);
}
});
}, []);

return (
<div className="product-section">
<h1>我們的商品(React 標題)</h1>
<div ref={containerRef} /> {/* Vue 組件會在這裡渲染 */}
</div>
);
};

跨框架通訊:讓 React 和 Vue 對話

當 React 和 Vue 需要「聊天」時,可以透過以下方式:

1. 自定義事件

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 handleProductSelection = (event) => {
console.log('用戶選擇了商品:', event.detail.product);
};

window.addEventListener('product-selected', handleProductSelection);
return () => window.removeEventListener('product-selected', handleProductSelection);
}, []);

2. 共享狀態管理

使用 Zustand 或其他狀態管理工具:

1
2
3
4
5
6
7
8
9
// 共享的狀態管理
import { create } from 'zustand';

const useSharedStore = create((set) => ({
selectedProduct: null,
setSelectedProduct: (product) => set({ selectedProduct: product }),
}));

// React 和 Vue 都可以使用這個 store

實務考量:什麼時候該用這招?

雖然 Module Federation 看起來很酷,但不是所有專案都適合:

適合的場景

  • 大型企業級應用:多個團隊各自負責不同模組
  • 漸進式遷移:從 Vue 2 升級到 Vue 3,或從 Vue 遷移到 React
  • 技術棧多樣化:團隊有不同的技術專長
  • 獨立部署需求:希望各模組能獨立發布更新

需要謹慎的場景

  • 小型專案:複雜度可能超過效益
  • 效能敏感應用:多框架載入會增加初始載入時間
  • 團隊技術單一:如果大家都熟悉同一個框架,沒必要混用

效能優化秘訣

1. 智慧的依賴共享

1
2
3
4
5
6
7
8
9
10
11
shared: {
'react': {
singleton: true, // 只載入一個 React 實例
eager: true, // 預先載入
requiredVersion: '^18.0.0'
},
'lodash': {
singleton: false, // 允許多個版本共存
shareScope: 'default'
}
}

2. 懶載入策略

1
2
3
4
5
6
// 只在需要時才載入 Vue 組件
const LazyVueComponent = React.lazy(() =>
import('productModule/ProductList').then(module => ({
default: () => <VueWrapper component={module.default} />
}))
);

3. 預載入重要模組

1
2
3
4
5
6
7
8
// 預先載入關鍵模組
const preloadModules = async () => {
await import('productModule/ProductList');
await import('cartModule/ShoppingCart');
};

// 在應用程式啟動時預載入
preloadModules();

開發體驗優化

TypeScript 支援

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 為遠端模組建立型別定義
declare module 'productModule/ProductList' {
const ProductList: any;
export default ProductList;
}

// 或使用更精確的型別定義
interface ProductListProps {
category?: string;
onProductSelect?: (product: Product) => void;
}

declare module 'productModule/ProductList' {
const ProductList: (props: ProductListProps) => void;
export default ProductList;
}

開發環境設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 開發環境下的 fallback 機制
const isDev = process.env.NODE_ENV === 'development';

const loadRemoteComponent = async (modulePath, fallback) => {
try {
return await import(modulePath);
} catch (error) {
if (isDev) {
console.warn(`無法載入遠端模組 ${modulePath},使用 fallback`);
return fallback;
}
throw error;
}
};

監控與除錯

載入狀態追蹤

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(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
import(modulePath)
.then(module => {
setComponent(module.default);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [modulePath]);

return { component, loading, error };
};

錯誤邊界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class RemoteComponentErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
return { hasError: true };
}

render() {
if (this.state.hasError) {
return <div>遠端組件載入失敗,請稍後再試</div>;
}

return this.props.children;
}
}

未來展望:前端架構的新可能

Module Federation + Vite 的組合,不只是技術上的創新,更是前端架構思維的轉變。它讓我們重新思考:

  • 團隊協作:不再需要統一技術棧,每個團隊都能發揮所長
  • 技術演進:可以漸進式地採用新技術,而不用推倒重來
  • 維護成本:模組化的架構讓維護更輕鬆,bug 影響範圍更小

當然,這個技術還在快速發展中。隨著 Module Federation Runtime 等新工具的出現,相信未來會有更多驚喜等著我們。

總結:擁抱多元,釋放創意

Vite + Module Federation 證明了一件事:技術不應該成為創意的枷鎖

當我們不再被「只能選擇一個框架」的思維限制時,前端開發的可能性變得無限寬廣。React 的生態系統、Vue 的簡潔優雅、Angular 的企業級特性...現在我們可以在同一個專案中享受它們的優點。

下次當有人問你「我們該用 React 還是 Vue?」時,你可以自信地回答:「為什麼不能兩個都用呢?」

記住,最好的架構不是最新的技術,而是最適合你團隊和專案需求的解決方案。Module Federation 給了我們更多選擇,但選擇權還是在我們手中。


想要親自體驗 Module Federation 的魔力嗎?建議先從小型 POC 開始,感受一下跨框架開發的樂趣。記住,技術是工具,創意才是靈魂!