天天看點

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

作者:雲謙

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

導讀:Federated Modules 是一個令人激動的功能,它可能會改變未來幾年的前端打包方式,作者深入分析了 Module Federation 的原理及其應用場景,希望能對大家有所啟發。

WHAT(Module Federation 是什麼?)

Module Federation [ˌfedəˈreɪʃn] 使 JavaScript 應用得以在用戶端或伺服器上動态運作另一個 bundle 的代碼。

這其中的關鍵點是:

  • 動态,包含兩個含義:
    • 按需,可以把一個包拆開來加載其中一部分;
    • 運作時,跑在浏覽器而非 node 編譯時;
  • 另一個 bundle 的代碼,之前應用之間做共享是在檔案級或 npm 包級 export 成員,現在可以在應用級 export 成員屬性。
三大應用場景調研,Webpack 新功能 Module Federation 深入解析

一些相關的概念:

  • Remote,被 Host 消費的 Webpack 建構;
  • Host,消費其他 Remote 的 Webpack 建構;

一個應用可以是 Host,也可以是 Remote,也可以同時是 Host 和 Remote。

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

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 沒有,再用自己的;
三大應用場景調研,Webpack 新功能 Module Federation 深入解析

産物:

三大應用場景調研,Webpack 新功能 Module Federation 深入解析
  • main.js,應用主檔案;
  • remoteEntry.js,作為 remote 時被引的檔案;
  • 一堆異步加載的檔案,main.js 或 remoteEntry.js 裡可能加載的檔案;

是以比如下面如圖示例的應用叢集:

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

加載方式應該這樣:

<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 可以用在哪裡?

微前端

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

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

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

方法一:讓每個子應用都分開打包,主應用不管,這樣不會有問題,但問題就是尺寸大,而且大了不是一點點。

方法二:主應用包含 antd 和 react,子應用如果版本一緻不打包 react 和 antd,版本不一緻就自己打一份,但有幾個問題:

  1. antd 和 react 是通過 umd 的方式同步載入的,主應用初始化會比較慢;
  2. 主應用更新了 antd 的時候,所有子應用可能需要一起更新,這個成本就很大了。

方法三:利用 Module Federation 的 shared 能力,子應用的依賴如果和主應用比對,那麼,能解決方法二裡的第一個問題,但第二個問題依舊解不了。

方法四:利用 Module Federation 的 remotes 能力,再提一個應用專門提供庫被消費,看起來前面的問題都能解。

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

有沒有感覺技術又輪回到了 seajs + spmjs 的時代。

應用叢集

微前端是應用叢集的解法之一,但不是唯一方案。

現狀是,通過 npm 共享元件。

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

基于 Module Federation,除通過 npm 共享依賴,還可以有運作時的依賴、元件、頁面甚至應用的直接共享。

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

這樣一來,靈活性就非常大了,可以在應用的各個層面做共享。A 應用引用 B 整個應用,也可以應用 B 的的頁面群組件,還可以提一個庫應用,做 npm 依賴的運作時共享。

編譯提速,應用秒開

我們大部分場景不是微前端或應用叢集,Module Federation 還可以幫助我們幹什麼?

現在項目組織和檔案依賴通常是這樣:

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

現狀是:

  • 全部打成一個包;
  • 打包時間較慢,據統計,内部雲編譯平台的平均編譯時間在 100s 以上;

期望的是:

  • node_modules 下的提前打包好,通過 runtime 的方式引;
  • 本地調試和編譯時隻打項目檔案;
  • 快,根據項目複雜度可提升到 1s - 7s 之内;

為什麼不是其他的編譯速度優化方案?

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

舉一個對比的例子,比如 external,我們之前還有做過自動的 external 方案,雖然他也可能顯著提速,但有以下問題:

  • 以空間換時間,依賴包全量引用導緻 npm,用在生産上會犧牲部分産品體驗,需權衡;
  • 不是所有的依賴都有 umd 包,覆寫率不夠;
  • npm 可能有依賴,比如 antd 依賴 react 和 moment,那麼 react 和 moment 也得 external 并且在html 裡引用他們;
  • 需要手動修改 html 裡的引用,維護上有成本提升。

更多參考:

三大應用場景調研,Webpack 新功能 Module Federation 深入解析

關注「Alibaba F2E」

把握阿裡巴巴前端新動向

繼續閱讀