Module Federation 通常譯作“模塊聯邦”,是 Webpack 5 新引入的一種遠程模塊動態加載、運行技術。MF 允許我們將原本單個巨大應用按我們理想的方式拆分成多個體積更小、職責更內聚的小應用形式,理想情況下各個應用能夠實現獨立部署、獨立開發(不同應用甚至允許使用不同技術棧)、團隊自治,從而降低系統與團隊協作的復雜度 —— 沒錯,這正是所謂的微前端架構。
An architectural style where independently deliverable frontend applications are composed into a greater whole —— 摘自《Micro Frontends》。
英文社區對 Webpack Module Federation 的響應非常熱烈,甚至被譽為“A game-changer in JavaScript architecture”,相對而言國內對此熱度并不高,這一方面是因為 MF 強依賴于 Webpack5,升級成本有點高;另一方面是國內已經有一些成熟微前端框架,例如 qiankun。不過我個人覺得 MF 有不少實用性強,非常值得學習、使用的特性,包括:
- 應用可按需導出若干模塊,這些模塊最終會被單獨打成模塊包,功能上有點像 NPM 模塊;
- 應用可在運行時基于 HTTP(S) 協議動態加載其它應用暴露的模塊,且用法與動態加載普通 NPM 模塊一樣簡單;
- 與其它微前端方案不同,MF 的應用之間關系平等,沒有主應用/子應用之分,每個應用都能導出/導入任意模塊;
- 等等。
圖片摘自:《Webpack 5 之 模塊聯合(Module Federation)》
簡單示例
Module Federation 的基本邏輯是一端導出模塊,另一端導入、使用模塊,實現上兩端都依賴于 Webpack 5 內置的 ModuleFederationPlugin
插件:
- 對于模塊生成方,需要使用
ModuleFederationPlugin
插件的expose
參數聲明需要導出的模塊列表; - 對于模塊使用方,需要使用
ModuleFederationPlugin
插件的remotes
參數聲明需要從哪些地方導入遠程模塊。
接下來,我們按這個流程一步步搭建一個簡單的 Webpack Module Federation 示例,首先介紹一下示例文件結構:
MF-basic
├─ app-1
│ ├─ dist
│ │ ├─ ...
│ ├─ package.json
│ ├─ src
│ │ ├─ main.js
│ │ ├─ foo.js
│ │ └─ utils.js
│ └─ webpack.config.js
├─ app-2
│ ├─ dist
│ │ ├─ ...
│ ├─ package.json
│ ├─ src
│ │ ├─ bootstrap.js
│ │ └─ main.js
│ ├─ webpack.config.js
├─ lerna.json
└─ package.json
提示:為簡化依賴管理,示例引入 lerna 實現 Monorepo 策略,不過這與文章主題無關,這里不做過多介紹。
其中,app-1
、app-2
是兩個獨立應用,分別有一套獨立的 Webpack 構建配置,類似于微前端場景下的“微應用”概念。在本示例中,app-1
負責導出模塊 —— 類似于子應用;app-2
負責使用這些模塊 —— 類似于主應用。
我們先看看模塊導出方 —— 也就是 app-1
的構建配置:
const path = require("path");
const { ModuleFederationPlugin } = require("webpack").container;module.exports = {mode: "development",devtool: false,entry: path.resolve(__dirname, "./src/main.js"),output: {path: path.resolve(__dirname, "./dist"),// 必須指定產物的完整路徑,否則使用方無法正確加載產物資源publicPath: `http://localhost:8081/dist/`,},plugins: [new ModuleFederationPlugin({// MF 應用名稱name: "app1",// MF 模塊入口,可以理解為該應用的資源清單filename: `remoteEntry.js`,// 定義應用導出哪些模塊exposes: {"./utils": "./src/utils","./foo": "./src/foo",},}),],// MF 應用資源提供方必須以 http(s) 形式提供服務// 所以這里需要使用 devServer 提供 http(s) server 能力devServer: {port: 8081,hot: true,},
};
提示:Module Federation 依賴于 Webpack5 內置的 ModuleFederationPlugin 實現模塊導入導出功能。
作用模塊導出方,app-1
的配置邏輯可以總結為:
- 需要使用
ModuleFederationPlugin
的exposes
項聲明哪些模塊需要被導出;使用filename
項定義入口文件名稱; - 需要使用
devServer
啟動開發服務器能力。
使用 ModuleFederationPlugin
插件后,Webpack 會將 exposes
聲明的模塊分別編譯為獨立產物,并將產物清單、MF 運行時等代碼打包進 filename
定義的應用入口文件(Remote Entry File)中。例如 app-1
經過 Webpack 編譯后,將生成如下產物:
MF-basic
├─ app-1
│ ├─ dist
│ │ ├─ main.js
│ │ ├─ remoteEntry.js
│ │ ├─ src_foo_js.js
│ │ └─ src_utils_js.js
│ ├─ src
│ │ ├─ ...
main.js
為整個應用的編譯結果,此處可忽略;src_utils_js.js
與src_foo_js.js
分別為exposes
聲明的模塊的編譯產物;remoteEntry.js
是ModuleFederationPlugin
插件生成的應用入口文件,包含模塊清單、MF 運行時代碼。
接下來繼續看看模塊導入方 —— 也就是 app-2
的配置方法:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;module.exports = {mode: "development",devtool: false,entry: path.resolve(__dirname, "./src/main.js"),output: {path: path.resolve(__dirname, "./dist"),},plugins: [// 模塊使用方也依然使用 ModuleFederationPlugin 插件搭建 MF 環境new ModuleFederationPlugin({// 使用 remotes 屬性聲明遠程模塊列表remotes: {// 地址需要指向導出方生成的應用入口文件RemoteApp: "app1@http://localhost:8081/dist/remoteEntry.js",},}),new HtmlWebpackPlugin(),],devServer: {port: 8082,hot: true,open: true,},
};
作用遠程模塊使用方,app-2
需要使用 ModuleFederationPlugin
聲明遠程模塊的 HTTP(S) 地址與模塊名稱(示例中的 RemoteApp
),之后在 app-2
中就可以使用模塊名稱異步導入 app-1
暴露出來的模塊,例如:
// app-2/src/main.js
(async () => {const { sayHello } = await import("RemoteApp/utils");sayHello();
})();
到這里,簡單示例就算是搭建完畢了,之后運行頁面,打開開發者工具的 Network 面板,可以看到:
其中:
remoteEntry.js
即app-1
構建的應用入口文件;src_utils_js.js
則是import("RemoteApp/utils")
語句導入的遠程模塊。
總結一下,MF 中的模塊導出/導入方都依賴于 ModuleFederationPlugin
插件,其中導出方需要使用插件的 exposes
項聲明導出哪些模塊,使用 filename
指定生成的入口文件;導入方需要使用 remotes
聲明遠程模塊地址,之后在代碼中使用異步導入語法 import("module")
引入模塊。
這種模塊遠程加載、運行的能力,搭配適當的 DevOps 手段,已經足以滿足微前端的獨立部署、獨立維護、開發隔離的要求,在此基礎上 MF 還提供了一套簡單的依賴共享功能,用于解決多應用間基礎庫管理問題。
依賴共享
上例應用相互獨立,各自管理、打包基礎依賴包,但實際項目中應用之間通常存在一部分公共依賴 —— 例如 Vue、React、Lodash 等,如果簡單沿用上例這種分開打包的方式勢必會出現依賴被重復打包,造成產物冗余的問題,為此 ModuleFederationPlugin
提供了 shared
配置用于聲明該應用可被共享的依賴模塊。
例如,改造上例模塊導出方 app-1
,添加 shared
配置:
module.exports = {// ...plugins: [new ModuleFederationPlugin({name: "app1",filename: `remoteEntry.js`,exposes: {"./utils": "./src/utils","./foo": "./src/foo",}, // 可被共享的依賴模塊
+ shared: ['lodash']}),],// ...
};
接下來,還需要修改模塊導入方 app-2
,添加相同的 shared
配置:
module.exports = {// ...plugins: [// 模塊使用方也依然使用 ModuleFederationPlugin 插件搭建 MF 環境new ModuleFederationPlugin({// 使用 remotes 屬性聲明遠程模塊列表remotes: {// 地址需要指向導出方生成的應用入口文件RemoteApp: "app1@http://localhost:8081/dist/remoteEntry.js",},
+ shared: ['lodash']}),new HtmlWebpackPlugin(),],// ...
};
之后,運行頁面可以看到最終只加載了一次 lodash
產物(下表左圖),而改動前則需要分別從導入/導出方各加載一次 lodash
(下表右圖):
添加 shared 后 | 改動前 |
---|---|
![]() | ![]() |
注意,這里要求兩個應用使用 版本號完全相同 的依賴才能被復用,假設上例應用 app-1
用了 lodash@4.17.0
,而 app-2
用的是 lodash@4.17.1
,Webpack 還是會同時加載兩份 lodash 代碼,我們可以通過 shared.[lib].requiredVersion
配置項顯式聲明應用需要的依賴庫版本來解決這個問題:
module.exports = {// ...plugins: [new ModuleFederationPlugin({// ...// 共享依賴及版本要求聲明
+ shared: {
+ lodash: {
+ requiredVersion: "^4.17.0",
+ },
+ },}),],// ...
};
上例 requiredVersion: "^4.17.0"
表示該應用支持共享版本大于等于 4.17.0
小于等于 4.18.0
的 lodash,其它應用所使用的 lodash 版本號只要在這一范圍內即可復用。requiredVersion
支持 Semantic Versioning 2.0 標準,這意味著我們可以復用 package.json
中聲明版本依賴的方法。
requiredVersion
的作用在于限制依賴版本的上下限,實用性極高。除此之外,我們還可以通過 shared.[lib].shareScope
屬性更精細地控制依賴的共享范圍,例如:
module.exports = {// ...plugins: [new ModuleFederationPlugin({// ...// 共享依賴及版本要求聲明
+ shared: {
+ lodash: {
+ // 任意字符串
+ shareScope: 'foo'
+ },
+ },}),],// ...
};
在這種配置下,其它應用所共享的 lodash 庫必須同樣聲明為 foo
空間才能復用。shareScope
在多團隊協作時能夠切分出多個資源共享空間,降低依賴沖突的概率。
除 requiredVersion
/shareScope
外,shared
還提供了一些不太常用的 配置,簡單介紹:
singletong
:強制約束多個版本之間共用同一個依賴包,如果依賴包不滿足版本requiredVersion
版本要求則報警告:
version
:聲明依賴包版本,缺省默認會從包體的package.json
的version
字段解析;packageName
:用于從描述文件中確定所需版本的包名稱,僅當無法從請求中自動確定包名稱時才需要這樣做;eager
:允許 webpack 直接打包該依賴庫 —— 而不是通過異步請求獲取庫;import
:聲明如何導入該模塊,默認為 shared 屬性名,實用性不高,可忽略。
示例:微前端
Module Federation 是一種非常新的技術,社區資料還比較少,接下來我們來編寫一個完整的微前端應用,幫助你更好理解 MF 的功能與用法。微前端架構通常包含一個作為容器的主應用及若干負責渲染具體頁面的子應用,分別對標到下面示例的 packages/host
與 packages/order
應用:
MF-micro-fe
├─ packages
│ ├─ host
│ │ ├─ public
│ │ │ └─ index.html
│ │ ├─ src
│ │ │ ├─ App.js
│ │ │ ├─ HomePage.js
│ │ │ ├─ Navigation.js
│ │ │ ├─ bootstrap.js
│ │ │ ├─ index.js
│ │ │ └─ routes.js
│ │ ├─ package.json
│ │ └─ webpack.config.js
│ └─ order
│ ├─ src
│ │ ├─ OrderDetail.js
│ │ ├─ OrderList.js
│ │ ├─ main.js
│ │ └─ routes.js
│ ├─ package.json
│ └─ webpack.config.js
├─ lerna.json
└─ package.json
提示:示例代碼已上傳到:MF-micro-fe,務必 Clone 下來輔助閱讀。
先看看 order
對應的 MF 配置:
module.exports = {// ...plugins: [new ModuleFederationPlugin({name: "order",filename: "remoteEntry.js",// 導入路由配置exposes: {"./routes": "./src/routes",},}),],
};
注意,order
應用實際導出的是路由配置文件 routes.js
。而 host
則通過 MF 插件導入并消費 order
應用的組件,對應配置:
module.exports = {// ...plugins: [// 模塊使用方也依然使用 ModuleFederationPlugin 插件搭建 MF 環境new ModuleFederationPlugin({// 使用 remotes 屬性聲明遠程模塊列表remotes: {// 地址需要指向導出方生成的應用入口文件RemoteOrder: "order@http://localhost:8081/dist/remoteEntry.js",},})],// ...
};
之后,在 host
應用中引入 order
的路由配置并應用到頁面中:
import localRoutes from "./routes";
// 引入遠程 order 模塊
import orderRoutes from "RemoteOrder/routes";const routes = [...localRoutes, ...orderRoutes];const App = () => (<React.StrictMode><HashRouter><h1>Micro Frontend Example</h1><Navigation /><Routes>{routes.map((route) => (<Routekey={route.path}path={route.path}element={<React.Suspense fallback={<>...</>}><route.component /></React.Suspense>}exact={route.exact}/>))}</Routes></HashRouter></React.StrictMode>
);export default App;
通過這種方式,一是可以將業務代碼分解為更細粒度的應用形態;二是應用可以各自管理路由邏輯,降低應用間耦合性。最終能降低系統組件間耦合度,更有利于多團隊協作。除此之外,MF 技術還有非常大想象空間,國外有大神專門整理了一系列實用 MF 示例:Module Federation Examples,感興趣的讀者務必仔細閱讀這些示例代碼。
總結
Module Federation 是 Webpack 5 新引入的一種遠程模塊動態加載、運行技術,雖然國內討論熱度較低,但使用簡單,功能強大,非常適用于微前端或代碼重構遷移場景。
使用上,只需引入 ModuleFederationPlugin
插件,按要求組織、分割好各個微應用的代碼,并正確配置 expose/remotes
配置項即可實現基于 HTTP(S) 的模塊共享功能。此外,我們還可以通過插件的 shared
配置項實現在應用間共享基礎依賴庫,還可以通過 shared.requireVersion
等一系列配置,精細控制依賴的共享版本與范圍。
總結
Module Federation 是 Webpack 5 新引入的一種遠程模塊動態加載、運行技術,雖然國內討論熱度較低,但使用簡單,功能強大,非常適用于微前端或代碼重構遷移場景。
使用上,只需引入 ModuleFederationPlugin
插件,按要求組織、分割好各個微應用的代碼,并正確配置 expose/remotes
配置項即可實現基于 HTTP(S) 的模塊共享功能。此外,我們還可以通過插件的 shared
配置項實現在應用間共享基礎依賴庫,還可以通過 shared.requireVersion
等一系列配置,精細控制依賴的共享版本與范圍。