Module Federation原理剖析


【轉自團隊掘金原文: 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學習目錄供大家參考:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM