|

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

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

通過(guò)回答 Module Federation 如何運(yùn)轉(zhuǎn)?Host 如何消費(fèi) Remote?以及 Remote 如何優(yōu)先使用 Host shared 的依賴(lài)?這三個(gè)問(wèn)題,我們分析一下 Module Federation 的原理。 整體是通過(guò) ModuleFederationPlugin(https://github.com/webpack/webpack/blob/dev-1/lib/container/ModuleFederationPlugin.js)這個(gè)插件串聯(lián)起來(lái)的。
配置示例: 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,作為輸出的模塊名,使用的時(shí)通過(guò) ${name}/${expose} 的方式使用;
library,必須,其中這里的 name 為作為 umd 的 name; remotes,可選,表示作為 Host 時(shí),去消費(fèi)哪些 Remote; exposes,可選,表示作為 Remote 時(shí),export 哪些屬性被消費(fèi); shared,可選,優(yōu)先用 Host 的依賴(lài),如果 Host 沒(méi)有,再用自己的;

產(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 的順序沒(méi)有要求,只要在 A/main.js 之前就好了。
可以通過(guò)代碼示例來(lái)進(jìn)行理解。
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)前項(xiàng)目的異步模塊
}
// 初始化
e('B/react');
這其中的原理:
A 如何讓 B 用 A shared 的庫(kù)? 再看如下兩個(gè)代碼示例。
B 構(gòu)建產(chǎn)物:
let B;
__webpack_require__.Overrides = {};
function e(moduleId) {
// 1. 取 shared 的模塊
// 當(dāng)前項(xiàng)目的 shared 模塊列表
const fallbackMapping = {};
// 先從 Overrides 里取,再?gòu)漠?dāng)前項(xiàng)目里取
push_require_try(__webpack_require__.Overrides[moduleId] || fallbackMapping[moduleId]);
// 2. 取 remote 的模塊
// 3. 取當(dāng)前項(xià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));
原理分析:
Remote(B)export override 方法,Host(A) 會(huì)調(diào)用其關(guān)聯(lián) Remote 的 override 方法,把 shared 的依賴(lài)寫(xiě)進(jìn)去; Remote(B) 獲取模塊時(shí)會(huì)優(yōu)先從 override 里取,沒(méi)有再?gòu)漠?dāng)前 Bundle 的模塊索引里?。?/p>
這樣,B 里面在 require react 時(shí),就會(huì)用 A 的 react 模塊。
WHY(它的應(yīng)用場(chǎng)景有哪些?)Module Federation 可以用在哪里?

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

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

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

基于 Module Federation,除通過(guò) npm 共享依賴(lài),還可以有運(yùn)行時(shí)的依賴(lài)、組件、頁(yè)面甚至應(yīng)用的直接共享。

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

現(xiàn)狀是:
期望的是:
node_modules 下的提前打包好,通過(guò) runtime 的方式引; 本地調(diào)試和編譯時(shí)只打項(xiàng)目文件; 快,根據(jù)項(xiàng)目復(fù)雜度可提升到 1s - 7s 之內(nèi);
為什么不是其他的編譯速度優(yōu)化方案?

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