Module Federation原理剖析

吉古力發表於2020-11-16

【轉自團隊掘金原文: https://juejin.im/post/6895324456668495880】

為什麼需要學習webpack5 module Federation原理呢?因為EMP微前端方案正是基於該革命性功能進行的,具有歷史突破意義。通過本文,可以讓你深入學習webpack5 module Federation原理,掌握EMP微前端方案的底層基石,更好使用和應用EMP微前端方案。

最近webpack5正式釋出,其中推出了一個非常令人激動的新功能,即今日的主角——Module Federation(以下簡稱為mf),下面將通過三個方面(what,how,where)來跟大家一起探索這個功能的奧祕。

一. 是什麼

Module Federation中文直譯為“模組聯邦”,而在webpack官方文件中,其實並未給出其真正含義,但給出了使用該功能的motivation, 即動機,原文如下

Multiple separate builds should form a single application. These separate builds should not have dependencies between each other, so they can be developed and deployed individually.

This is often known as Micro-Frontends, but is not limited to that.

翻譯成中文即

多個獨立的構建可以形成一個應用程式。這些獨立的構建不會相互依賴,因此可以單獨開發和部署它們。
這通常被稱為微前端,但並不僅限於此。

結合以上,不難看出,mf實際想要做的事,便是把多個無相互依賴、單獨部署的應用合併為一個。通俗點講,即mf提供了能在當前應用中遠端載入其他伺服器上應用的能力。對此,可以引出下面兩個概念:

  • host:引用了其他應用的應用
  • remote:被其他應用所使用的應用

 

鑑於mf的能力,我們可以完全實現一個去中心化的應用部署群:每個應用是單獨部署在各自的伺服器,每個應用都可以引用其他應用,也能被其他應用所引用,即每個應用可以充當host的角色,亦可以作為remote出現,無中心應用的概念。 

二. 如何使用

配置示例:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  // 其他webpack配置...
  plugins: [
    new ModuleFederationPlugin({
        name: 'empBase',
        library: { type: 'var', name: 'empBase' },
        filename: 'emp.js',
        remotes: {
          app_two: "app_two_remote",
          app_three: "app_three_remote"
        },
        exposes: {
          './Component1': 'src/components/Component1',
          './Component2': 'src/components/Component2',
        },
        shared: ["react", "react-dom","react-router-dom"]
      })
  ]
}

 

通過以上配置,我們對mf有了一個初步的認識,即如果要使用mf,需要配置好幾個重要的屬性:

欄位名型別含義
name string 必傳值,即輸出的模組名,被遠端引用時路徑為${name}/${expose}
library object 宣告全域性變數的方式,name為umd的name
filename string 構建輸出的檔名
remotes object 遠端引用的應用名及其別名的對映,使用時以key值作為name
exposes object 被遠端引用時可暴露的資源路徑及其別名
shared object 與其他應用之間可以共享的第三方依賴,使你的程式碼中不用重複載入同一份依賴

三. 構建解析原理

讓我們看看構建後的程式碼:

var moduleMap = {
    "./components/Comonpnent1": function() {
        return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_react_react"), __webpack_require__.e("src_components_Close_index_tsx")]).then(function() { return function() { return (__webpack_require__(16499)); }; });
    },
};
var get = function(module, getScope) {
    __webpack_require__.R = getScope;
    getScope = (
        __webpack_require__.o(moduleMap, module)
            ? moduleMap[module]()
            : Promise.resolve().then(function() {
                throw new Error('Module "' + module + '" does not exist in container.');
            })
    );
    __webpack_require__.R = undefined;
    return getScope;
};
var init = function(shareScope, initScope) {
    if (!__webpack_require__.S) return;
    var oldScope = __webpack_require__.S["default"];
    var name = "default"
    if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
    __webpack_require__.S[name] = shareScope;
    return __webpack_require__.I(name, initScope);
}

 

可以看到,程式碼中包括三個部分:

  • moduleMap:通過exposes生成的模組集合
  • get: host通過該函式,可以拿到remote中的元件
  • init:host通過該函式將依賴注入remote中

再看moduleMap,返回對應元件前,先通過__webpack_require__.e載入了其對應的依賴,讓我們看看__webpack_require__.e做了什麼:

__webpack_require__.f = {};
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function(chunkId) {
    // 獲取__webpack_require__.f中的依賴
  return Promise.all(Object.keys(__webpack_require__.f).reduce(function(promises, key) {
    __webpack_require__.f[key](chunkId, promises);
     return promises;
 }, []));
};
__webpack_require__.f.consumes = function(chunkId, promises) {
// 檢查當前需要載入的chunk是否是在配置項中被宣告為shared共享資源,如果在__webpack_require__.O上能找到對應資源,則直接使用,不再去請求資源
 if(__webpack_require__.o(chunkMapping, chunkId)) {
     chunkMapping[chunkId].forEach(function(id) {
         if(__webpack_require__.o(installedModules, id)) return promises.push(installedModules[id]);
         var onFactory = function(factory) {
             installedModules[id] = 0;
             __webpack_modules__[id] = function(module) {
                 delete __webpack_module_cache__[id];
                 module.exports = factory();
             }
         };
         try {
             var promise = moduleToHandlerMapping[id]();
             if(promise.then) {
                 promises.push(installedModules[id] = promise.then(onFactory).catch(onError));
             } else onFactory(promise);
         } catch(e) { onError(e); }
     });
 }
}

 

通讀核心程式碼之後,可以得到如下總結:

  • 首先,mf會讓webpack以filename作為檔名生成檔案
  • 其次,檔案中以var的形式暴露了一個名為name的全域性變數,其中包含了exposes以及shared中配置的內容
  • 最後,作為host時,先通過remoteinit方法將自身shared寫入remote中,再通過get獲取remoteexpose的元件,而作為remote時,判斷host中是否有可用的共享依賴,若有,則載入host的這部分依賴,若無,則載入自身依賴。

四. 應用場景

英雄也怕無用武之地,讓我們看看mf的應用場景有哪些:

  • 微前端:通過shared以及exposes可以將多個應用引入同一應用中進行管理,由YY業務中臺web前端組團隊自主研發的EMP微前端方案就是基於mf的能力而實現的。
  • 資源複用,減少編譯體積:可以將多個應用都用到的通用元件單獨部署,通過mf的功能在runtime時引入到其他專案中,這樣元件程式碼就不會編譯到專案中,同時亦能滿足多個專案同時使用的需求,一舉兩得。

五. 最後

目前僅有EMP微前端方案是基於Module Federation實現的一套具有成熟腳手架和完整生態的微前端方案,並且在歡聚時代公司內部應用了80%的大型專案,通過本文我們也可以認知到EMP微前端方案是具有前瞻性的、可擴充套件性的、基石可靠的。針對EMP微前端方案的學習,有完整的wiki學習目錄供大家參考:

相關文章