作者:雲謙
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5iMmZmZzMTZjVGZ5EzMxYWYjhzN0YWY0QzYyImMwIjYz8CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.png)
導讀:Federated Modules 是一個令人激動的功能,它可能會改變未來幾年的前端打包方式,作者深入分析了 Module Federation 的原理及其應用場景,希望能對大家有所啟發。
WHAT(Module Federation 是什麼?)
Module Federation [ˌfedəˈreɪʃn] 使 JavaScript 應用得以在用戶端或伺服器上動态運作另一個 bundle 的代碼。
這其中的關鍵點是:
- 動态,包含兩個含義:
- 按需,可以把一個包拆開來加載其中一部分;
- 運作時,跑在浏覽器而非 node 編譯時;
- 另一個 bundle 的代碼,之前應用之間做共享是在檔案級或 npm 包級 export 成員,現在可以在應用級 export 成員屬性。
一些相關的概念:
- Remote,被 Host 消費的 Webpack 建構;
- Host,消費其他 Remote 的 Webpack 建構;
一個應用可以是 Host,也可以是 Remote,也可以同時是 Host 和 Remote。
HOW(它的原理是什麼?)
通過回答 Module Federation 如何運轉?Host 如何消費 Remote?以及 Remote 如何優先使用 Host shared 的依賴?這三個問題,我們分析一下 Module Federation 的原理。
ModuleFederationPlugin
整體是通過
這個插件串聯起來的。
配置示例:
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,可選,優先用 Host 的依賴,如果 Host 沒有,再用自己的;
産物:
- main.js,應用主檔案;
- remoteEntry.js,作為 remote 時被引的檔案;
- 一堆異步加載的檔案,main.js 或 remoteEntry.js 裡可能加載的檔案;
是以比如下面如圖示例的應用叢集:
加載方式應該這樣:
<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 之前就好了。
A 如何消費 B ?
可以通過代碼示例來進行了解。
B 源碼:
// src/react.js
export * from 'react';
// webpack.config.js
...
exposes: {
react: './src/react',
},
A 源碼:
// 異步加載 B 的 react 子產品
const React = await import('B/react');
B 建構産物:
// windows 變量
let B;
const moduleMap = {
'react': () => {
return Promise.all([e('a'), e('b'), e('c')]),
},
};
B = {
get(moduleId) {
return moduleMap(moduleId);
}
}
A 建構産物:
const modules = {
'B': () => {
return B;
}
};
// 異步擷取子產品 export 内容
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. 取目前項目的異步子產品
}
// 初始化
e('B/react');
這其中的原理:
- 多個 Bundler 之間通過全局變量串聯;
- Remote 會 export get 方法擷取他的子子產品,子子產品的擷取通過 Promise 以按需的方式引入;
A 如何讓 B 用 A shared 的庫?
再看如下兩個代碼示例。
let B;
__webpack_require__.Overrides = {};
function e(moduleId) {
// 1. 取 shared 的子產品
// 目前項目的 shared 子產品清單
const fallbackMapping = {};
// 先從 Overrides 裡取,再從目前項目裡取
push_require_try(__webpack_require__.Overrides[moduleId] || fallbackMapping[moduleId]);
// 2. 取 remote 的子產品
// 3. 取目前項目的異步子產品
}
B = {
override(override) {
Object.assign(__webpack_require__.Overrides, override);
}
}
B.override(Object.assign({
'react': () => {
// A 的 react 内容
},
}, __webpack_require__.Overrides));
原理分析:
- Remote(B)export override 方法,Host(A) 會調用其關聯 Remote 的 override 方法,把 shared 的依賴寫進去;
- Remote(B) 擷取子產品時會優先從 override 裡取,沒有再從目前 Bundle 的子產品索引裡取;
這樣,B 裡面在 require react 時,就會用 A 的 react 子產品。
WHY(它的應用場景有哪些?)
Module Federation 可以用在哪裡?
微前端
如上圖,這是去年畫的一張微前端的圖,其中最下面的 “公共依賴加載” 一直是沒有非常優雅的方案。
方法一:讓每個子應用都分開打包,主應用不管,這樣不會有問題,但問題就是尺寸大,而且大了不是一點點。
方法二:主應用包含 antd 和 react,子應用如果版本一緻不打包 react 和 antd,版本不一緻就自己打一份,但有幾個問題:
- antd 和 react 是通過 umd 的方式同步載入的,主應用初始化會比較慢;
- 主應用更新了 antd 的時候,所有子應用可能需要一起更新,這個成本就很大了。
方法三:利用 Module Federation 的 shared 能力,子應用的依賴如果和主應用比對,那麼,能解決方法二裡的第一個問題,但第二個問題依舊解不了。
方法四:利用 Module Federation 的 remotes 能力,再提一個應用專門提供庫被消費,看起來前面的問題都能解。
有沒有感覺技術又輪回到了 seajs + spmjs 的時代。
應用叢集
微前端是應用叢集的解法之一,但不是唯一方案。
現狀是,通過 npm 共享元件。
基于 Module Federation,除通過 npm 共享依賴,還可以有運作時的依賴、元件、頁面甚至應用的直接共享。
這樣一來,靈活性就非常大了,可以在應用的各個層面做共享。A 應用引用 B 整個應用,也可以應用 B 的的頁面群組件,還可以提一個庫應用,做 npm 依賴的運作時共享。
編譯提速,應用秒開
我們大部分場景不是微前端或應用叢集,Module Federation 還可以幫助我們幹什麼?
現在項目組織和檔案依賴通常是這樣:
現狀是:
- 全部打成一個包;
- 打包時間較慢,據統計,内部雲編譯平台的平均編譯時間在 100s 以上;
期望的是:
- node_modules 下的提前打包好,通過 runtime 的方式引;
- 本地調試和編譯時隻打項目檔案;
- 快,根據項目複雜度可提升到 1s - 7s 之内;
為什麼不是其他的編譯速度優化方案?
舉一個對比的例子,比如 external,我們之前還有做過自動的 external 方案,雖然他也可能顯著提速,但有以下問題:
- 以空間換時間,依賴包全量引用導緻 npm,用在生産上會犧牲部分産品體驗,需權衡;
- 不是所有的依賴都有 umd 包,覆寫率不夠;
- npm 可能有依賴,比如 antd 依賴 react 和 moment,那麼 react 和 moment 也得 external 并且在html 裡引用他們;
- 需要手動修改 html 裡的引用,維護上有成本提升。
更多參考:
- https://richlab.design/translations/2020/03/27/webpack-5-module-federation/
- https://dev.to/marais/webpack-5-and-module-federation-4j1i
- https://github.com/Paciolan/remote-component
- https://federated-libraries.now.sh/
- https://github.com/jacob-ebey/federated-libraries-get-started
- https://twitter.com/ScriptedAlchemy
- https://twitter.com/codervandal
- https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651236238&idx=1&sn=fe46cf50030b8a7ae917b9c8c7de601c&chksm=bd497e0a8a3ef71c8c3ea67ef24603994659fa1f55a1ebb488bba135b0138125620b4100f831#rd
- https://github.com/ScriptedAlchemy/webpack-external-import
- https://github.com/webpack/webpack/projects/6
- https://github.com/webpack/webpack/issues/10352
關注「Alibaba F2E」
把握阿裡巴巴前端新動向