
導(dǎo)讀:Federated Modules 是一個令人激動的功能,它可能會改變未來幾年的前端打包方式,作者深入分析了 Module Federation 的原理及其應(yīng)用場景,希望能對大家有所啟發(fā)。 WHAT(Module Federation 是什么?)Module Federation [?fed??re??n] 使 JavaScript 應(yīng)用得以在客戶端或服務(wù)器上動態(tài)運行另一個 bundle 的代碼。
這其中的關(guān)鍵點是:

一些相關(guān)的概念:
一個應(yīng)用可以是 Host,也可以是 Remote,也可以同時是 Host 和 Remote。

通過回答 Module Federation 如何運轉(zhuǎn)?Host 如何消費 Remote?以及 Remote 如何優(yōu)先使用 Host shared 的依賴?這三個問題,我們分析一下 Module Federation 的原理。 整體是通過 ModuleFederationPlugin(https://github.com/webpack/webpack/blob/dev-1/lib/container/ModuleFederationPlugin.js)這個插件串聯(lián)起來的。
配置示例: new ModuleFederationPlugin({
name: 'app-1',
library: { type: 'var', name: 'app_1' },
filename: 'remoteEntry.js',
remotes: {
app_02: 'app_02',
app_03: 'app_03',
},
exposes: {
antd: './src/antd',
button: './src/button',
},
shared: ['react', 'react-dom'],
}),
配置屬性:
name,必須,唯一 ID,作為輸出的模塊名,使用的時通過 ${name}/${expose} 的方式使用;
library,必須,其中這里的 name 為作為 umd 的 name; remotes,可選,表示作為 Host 時,去消費哪些 Remote; exposes,可選,表示作為 Remote 時,export 哪些屬性被消費; shared,可選,優(yōu)先用 Host 的依賴,如果 Host 沒有,再用自己的;

產(chǎn)物:

所以比如下面如圖示例的應(yīng)用集群:

加載方式應(yīng)該這樣: <script src='C/remoteEntry.js'></script>
<script src='B/remoteEntry.js'></script>
<script src='A/main.js'></script
C/remoteEntry.js 和 B/remoteEntry 的順序沒有要求,只要在 A/main.js 之前就好了。
可以通過代碼示例來進行理解。
B 源碼: // src/react.js
export * from 'react';
// webpack.config.js
...
exposes: {
react: './src/react',
},
A 源碼:
// 異步加載 B 的 react 模塊
const React = await import('B/react');
B 構(gòu)建產(chǎn)物:
// windows 變量
let B;
const moduleMap = {
'react': () => {
return Promise.all([e('a'), e('b'), e('c')]),
},
};
B = {
get(moduleId) {
return moduleMap(moduleId);
}
}
A 構(gòu)建產(chǎn)物:
const modules = {
'B': () => {
return B;
}
};
// 異步獲取模塊 export 內(nèi)容
function e(moduleId) {
// 1. 取 shared 的模塊
// 2. 取 remote 的模塊
const idToExternalAndNameMapping = {
'B/react': ['B', 'react'],
};
// 從 module B 里取 react
const data = idToExternalAndNameMapping[moduleId];
__webpack_require__(data[0]).get(data[1]);
// 3. 取當(dāng)前項目的異步模塊
}
// 初始化
e('B/react');
這其中的原理:
再看如下兩個代碼示例。
B 構(gòu)建產(chǎn)物:
let B;
__webpack_require__.Overrides = {};
function e(moduleId) {
// 1. 取 shared 的模塊
// 當(dāng)前項目的 shared 模塊列表
const fallbackMapping = {};
// 先從 Overrides 里取,再從當(dāng)前項目里取
push_require_try(__webpack_require__.Overrides[moduleId] || fallbackMapping[moduleId]);
// 2. 取 remote 的模塊
// 3. 取當(dāng)前項目的異步模塊
}
B = {
override(override) {
Object.assign(__webpack_require__.Overrides, override);
}
}
A 構(gòu)建產(chǎn)物:
B.override(Object.assign({
'react': () => {
// A 的 react 內(nèi)容
},
}, __webpack_require__.Overrides));
原理分析:
這樣,B 里面在 require react 時,就會用 A 的 react 模塊。
Module Federation 可以用在哪里?

如上圖,這是去年畫的一張微前端的圖,其中最下面的 “公共依賴加載” 一直是沒有非常優(yōu)雅的方案。

方法一:讓每個子應(yīng)用都分開打包,主應(yīng)用不管,這樣不會有問題,但問題就是尺寸大,而且大了不是一點點。
方法二:主應(yīng)用包含 antd 和 react,子應(yīng)用如果版本一致不打包 react 和 antd,版本不一致就自己打一份,但有幾個問題:
antd 和 react 是通過 umd 的方式同步載入的,主應(yīng)用初始化會比較慢; 主應(yīng)用升級了 antd 的時候,所有子應(yīng)用可能需要一起升級,這個成本就很大了。
方法三:利用 Module Federation 的 shared 能力,子應(yīng)用的依賴如果和主應(yīng)用匹配,那么,能解決方法二里的第一個問題,但第二個問題依舊解不了。
方法四:利用 Module Federation 的 remotes 能力,再提一個應(yīng)用專門提供庫被消費,看起來前面的問題都能解。

有沒有感覺技術(shù)又輪回到了 seajs + spmjs 的時代。
微前端是應(yīng)用集群的解法之一,但不是唯一方案。
現(xiàn)狀是,通過 npm 共享組件。

基于 Module Federation,除通過 npm 共享依賴,還可以有運行時的依賴、組件、頁面甚至應(yīng)用的直接共享。

這樣一來,靈活性就非常大了,可以在應(yīng)用的各個層面做共享。A 應(yīng)用引用 B 整個應(yīng)用,也可以應(yīng)用 B 的的頁面和組件,還可以提一個庫應(yīng)用,做 npm 依賴的運行時共享。
我們大部分場景不是微前端或應(yīng)用集群,Module Federation 還可以幫助我們干什么?
現(xiàn)在項目組織和文件依賴通常是這樣:

現(xiàn)狀是:
期望的是:
為什么不是其他的編譯速度優(yōu)化方案?

舉一個對比的例子,比如 external,我們之前還有做過自動的 external 方案,雖然他也可能顯著提速,但有以下問題:
以空間換時間,依賴包全量引用導(dǎo)致 npm,用在生產(chǎn)上會犧牲部分產(chǎn)品體驗,需權(quán)衡; 不是所有的依賴都有 umd 包,覆蓋率不夠; npm 可能有依賴,比如 antd 依賴 react 和 moment,那么 react 和 moment 也得 external 并且在html 里引用他們; 需要手動修改 html 里的引用,維護上有成本提升。
更多參考:
關(guān)注「Alibaba F2E」 把握阿里巴巴前端新動向
|