使用 Webpack 優雅的構建微前端應用?

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 的應用之間關系平等,沒有主應用/子應用之分,每個應用都能導出/導入任意模塊;
  • 等等。

image.png

圖片摘自:《Webpack 5 之 模塊聯合(Module Federation)》

簡單示例

Module Federation 的基本邏輯是一端導出模塊,另一端導入、使用模塊,實現上兩端都依賴于 Webpack 5 內置的 ModuleFederationPlugin 插件:

  1. 對于模塊生成方,需要使用 ModuleFederationPlugin 插件的 expose 參數聲明需要導出的模塊列表;
  2. 對于模塊使用方,需要使用 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-1app-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 的配置邏輯可以總結為:

  1. 需要使用 ModuleFederationPluginexposes 項聲明哪些模塊需要被導出;使用 filename 項定義入口文件名稱;
  2. 需要使用 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.jssrc_foo_js.js 分別為 exposes 聲明的模塊的編譯產物;
  • remoteEntry.jsModuleFederationPlugin 插件生成的應用入口文件,包含模塊清單、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 面板,可以看到:

image.png

其中:

  • remoteEntry.jsapp-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改動前
imgimg

注意,這里要求兩個應用使用 版本號完全相同 的依賴才能被復用,假設上例應用 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 版本要求則報警告:

image.png

  • version:聲明依賴包版本,缺省默認會從包體的 package.jsonversion 字段解析;
  • packageName:用于從描述文件中確定所需版本的包名稱,僅當無法從請求中自動確定包名稱時才需要這樣做;
  • eager:允許 webpack 直接打包該依賴庫 —— 而不是通過異步請求獲取庫;
  • import:聲明如何導入該模塊,默認為 shared 屬性名,實用性不高,可忽略。

示例:微前端

Module Federation 是一種非常新的技術,社區資料還比較少,接下來我們來編寫一個完整的微前端應用,幫助你更好理解 MF 的功能與用法。微前端架構通常包含一個作為容器的主應用及若干負責渲染具體頁面的子應用,分別對標到下面示例的 packages/hostpackages/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 等一系列配置,精細控制依賴的共享版本與范圍。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/64271.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/64271.shtml
英文地址,請注明出處:http://en.pswp.cn/web/64271.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Boost之log日志使用

不講理論&#xff0c;直接上在程序中可用代碼&#xff1a; 一、引入Boost模塊 開發環境&#xff1a;Visual Studio 2017 Boost庫版本&#xff1a;1.68.0 安裝方式&#xff1a;Nuget 安裝命令&#xff1a; #只安裝下面幾個即可 Install-package boost -version 1.68.0 Install…

【MySQL】十四,MySQL 8.0的隱藏索引

在MySQL 8.0之前的版本中&#xff0c;索引只能直接刪除。如果刪除后發現引起了系統故障&#xff0c;又必須進行創建。當表的數據量比較大的時候&#xff0c;這樣做的代價就會非常高。 在MySQL 8.0中&#xff0c;提供了隱藏索引。如果想刪除某個索引&#xff0c;那么在實際刪除…

【ES6復習筆記】解構賦值(2)

介紹 解構賦值是一種非常方便的語法&#xff0c;可以讓我們更簡潔地從數組和對象中提取值&#xff0c;并且可以應用于很多實際開發場景中。 1. 數組的解構賦值 數組的解構賦值是按照一定模式從數組中提取值&#xff0c;然后對變量進行賦值。下面是一個例子&#xff1a; con…

爬蟲數據存儲:Redis、MySQL 與 MongoDB 的對比與實踐

爬蟲的核心任務是從網絡中提取數據&#xff0c;而存儲這些數據是流程中不可或缺的一環。根據業務需求的不同&#xff0c;存儲的選擇可能直接影響數據處理的效率和開發體驗。本文將介紹三種常用的存儲工具——Redis、MySQL 和 MongoDB&#xff0c;分析它們的特點&#xff0c;并提…

【Python】使用匿名函數Lambda解析html源碼的任意元素(Seleinium ,BeautifulSoup皆適用)

一直都發現lambda函數非常好用&#xff0c;它可以用簡潔的方式編寫小函數&#xff0c;無需寫冗長的過程就可以獲取結果。干脆利落&#xff01; 它允許我們定義一個匿名函數&#xff0c;在調用一次性的函數時非常有用。 最近整理了一些&#xff0c;lambda函數結合BeautifulSou…

Bash語言的語法

Bash語言簡介與應用 Bash&#xff08;Bourne Again SHell&#xff09;是一種Unix Shell和命令語言&#xff0c;在Linux、macOS及其他類Unix系統中被廣泛使用。作為GNU項目的一部分&#xff0c;Bash不僅是對早期Bourne Shell的增強&#xff0c;還引入了許多特性和功能&#xff…

Ingress-Nginx Annotations 指南:配置要點全方面解讀(下)

文章目錄 1.HTTP2 Push Preload2.Server Alias3.Server snippet4.Client Body Buffer Size5.External Authentication6.Global External Authentication7.Rate Limiting8.Global Rate Limiting9.Permanent Redirect10.Permanent Redirect Code11.Temporal Redirect12.SSL Passt…

互聯網路由架構

大家覺得有意義和幫助記得及時關注和點贊!!! 本書致力于解決實際問題&#xff0c;書中包含大量的架構圖、拓撲圖和真實場景示例&#xff0c;內容全面 且易于上手&#xff0c;是不可多得的良心之作。本書目的是使讀者成為將自有網絡集成到全球互聯網 領域的專家。 以下是筆記內…

【Flutter_Web】Flutter編譯Web第三篇(網絡請求篇):dio如何改造方法,變成web之后數據如何處理

前言 Flutter端在處理網絡請求的時候&#xff0c;最常用的庫當然是Dio了&#xff0c;那么在改造成web端的時候&#xff0c;最先處理的必然是網絡請求&#xff0c;否則沒有數據去處理驅動實圖渲染。 官方鏈接 pub https://pub.dev/packages/diogithub https://github.com/c…

Spring Boot @Conditional注解

在Spring Boot中&#xff0c;Conditional 注解用于條件性地注冊bean。這意味著它可以根據某些條件來決定是否應該創建一個特定的bean。這個注解可以放在配置類或方法上&#xff0c;并且它會根據提供的一組條件來判斷是否應該實例化對應的組件。 要使用 Conditional注解時&#…

項目上傳到gitcode

首先需要在個人設置里面找到令牌 記住自己的賬號和訪問令牌&#xff08;一長串&#xff09;&#xff0c;后面git要輸入這個&#xff0c; 賬號是下面這個 來到自己的倉庫 #查看遠程倉庫&#xff0c;是不是自己的云倉庫 git remote -v # 創建新分支 git checkout -b llf # 三步…

【Rust自學】6.4. 簡單的控制流-if let

喜歡的話別忘了點贊、收藏加關注哦&#xff0c;對接下來的教程有興趣的可以關注專欄。謝謝喵&#xff01;(&#xff65;ω&#xff65;) 6.4.1. 什么是if let if let語法允許將if和let組合成一種不太冗長的方式來處理與一種模式匹配的值&#xff0c;同時忽略其余模式。 可以…

【Git學習】windows系統下git init后沒有看到生成的.git文件夾

[問題] git init 命令后看不到.git文件夾 [原因] 文件夾設置隱藏 [解決辦法] Win11 win10

vscode添加全局宏定義

利用vscode編輯代碼時&#xff0c;設置了禁用非活動區域著色后&#xff0c;在一些編譯腳本中配置的宏又識別不了 遇到#ifdef包住的代碼就會變暗色&#xff0c;想查看代碼不是很方便。如下圖&#xff1a; 一 解決&#xff1a; 在vscode中添加全局宏定義。 二 步驟&#xff1a…

【服務器主板】定制化:基于Intel至強平臺的全新解決方案

隨著數據處理需求不斷增長&#xff0c;服務器硬件的發展也在持續推進。在這一背景下&#xff0c;為用戶定制了一款全新的基于Intel至強平臺的服務器主板&#xff0c;旨在提供強大的計算能力、優異的內存支持以及高速存儲擴展能力。適用于需要高性能計算、大規模數據處理和高可用…

php怎么去除數點后面的0

在PHP中&#xff0c;我們可以使用幾種方法來去除數字小數點后的0。 方法一&#xff1a;使用intval函數 intval函數可以將一個數字轉化為整數&#xff0c;另外&#xff0c;它也可以去除小數點后面的0。 “php $number 123.4500; $number intval($number); echo $number; // 輸…

數字后端培訓項目Floorplan常見問題系列專題續集1

今天繼續給大家分享下數字IC后端設計實現floorplan階段常見問題系列專題。這些問題都是來自于咱們社區IC后端訓練營學員提問的問題庫。目前這部分問題庫已經積累了4年了&#xff0c;后面會陸續分享這方面的問題。 希望對大家的數字后端學習和工作有所幫助。 數字后端項目Floor…

【遞歸,搜索與回溯算法 綜合練習】深入理解暴搜決策樹:遞歸,搜索與回溯算法綜合小專題(二)

優美的排列 題目解析 算法原理 解法 &#xff1a;暴搜 決策樹 紅色剪枝&#xff1a;用于剪去該節點的值在對應分支中&#xff0c;已經被使用的情況&#xff0c;可以定義一個 check[ ] 紫色剪枝&#xff1a;perm[i] 不能夠被 i 整除&#xff0c;i 不能夠被 per…

Java中各種數組復制方式的效率對比

在 Java 中&#xff0c;數組復制是一個常見的操作&#xff0c;尤其是在處理動態數組&#xff08;如 ArrayList&#xff09;時。Java 提供了多種數組復制的方式&#xff0c;每種方式在性能和使用場景上都有所不同。以下是對幾種主要數組復制方式的比較&#xff0c;包括 System.a…

視頻會議是如何實現屏幕標注功能的?

現在主流的視頻會議軟件都有屏幕標注功能&#xff0c;屏幕標注功能給屏幕分享者講解分享內容時提供了極大的方便。那我們以傲瑞視頻會議&#xff08;OrayMeeting&#xff09;為例&#xff0c;來講解屏幕標注是如何實現的。 傲瑞會議的PC端&#xff08;Windows、信創Linux、銀河…