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
時,先通過remote
的init
方法將自身shared
寫入remote
中,再通過get
獲取remote
中expose
的元件,而作為remote
時,判斷host
中是否有可用的共享依賴,若有,則載入host
的這部分依賴,若無,則載入自身依賴。
四. 應用場景
英雄也怕無用武之地,讓我們看看mf的應用場景有哪些:
- 微前端:通過shared以及exposes可以將多個應用引入同一應用中進行管理,由YY業務中臺web前端組團隊自主研發的EMP微前端方案就是基於mf的能力而實現的。
- 資源複用,減少編譯體積:可以將多個應用都用到的通用元件單獨部署,通過mf的功能在runtime時引入到其他專案中,這樣元件程式碼就不會編譯到專案中,同時亦能滿足多個專案同時使用的需求,一舉兩得。
五. 最後
目前僅有EMP微前端方案是基於Module Federation實現的一套具有成熟腳手架和完整生態的微前端方案,並且在歡聚時代公司內部應用了80%的大型專案,通過本文我們也可以認知到EMP微前端方案是具有前瞻性的、可擴充套件性的、基石可靠的。針對EMP微前端方案的學習,有完整的wiki學習目錄供大家參考: