新聞中心
模塊共享的方案對比場景:目前擁有項(xiàng)目 A 和項(xiàng)目 B,我們發(fā)現(xiàn)它們存在一定的共性,比如公共 UI 組件、utils 等。那我們?nèi)绾瓮磉@些公共信息呢?

創(chuàng)新互聯(lián)公司是專業(yè)的雁江網(wǎng)站建設(shè)公司,雁江接單;提供網(wǎng)站設(shè)計(jì)、成都做網(wǎng)站,網(wǎng)頁設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行雁江網(wǎng)站開發(fā)網(wǎng)頁制作和功能擴(kuò)展;專業(yè)做搜索引擎喜愛的網(wǎng)站,專業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來合作!
簡單粗暴-CV大法
直接將項(xiàng)目 A 的組件,copy 到項(xiàng)目 B 中,這樣的方式有時(shí)候是比較快的,但也存在維護(hù)性極低的問題,后續(xù)兩個(gè)項(xiàng)目都各自維護(hù)一套。
抽象成 npm
我們可以將一些公共的模塊抽象成 npm,每個(gè)項(xiàng)目都去安裝該 npm 包,從而達(dá)到共享的目的
但是 npm 包的方式存在以下問題:
- 編譯與構(gòu)建:一些公共的工具庫,框架以及 UI 庫存在重復(fù)構(gòu)建,造成性能低下。
- 版本更新:需要各個(gè)項(xiàng)目都去升級。“發(fā)布 -> 通知 -> 更新” 的方式是比較低效率的。
CDN + webpack externals
跟 npm 類似,只不過將其上傳到 CDN,通過結(jié)合 webpack externals 進(jìn)行加載,除了上面提到的問題,還有 externals 沒有按需加載。
git submodule
子模塊允許你將一個(gè) Git 倉庫作為另一個(gè) Git 倉庫的子目錄。它能讓你將另一個(gè)倉庫克隆到自己的項(xiàng)目中,同時(shí)還保持提交的獨(dú)立。
其還是會存在重復(fù)構(gòu)建的問題,而且還會一定的上手成本。
相關(guān)的命令:
- git submodule add <子模塊repository>: 添加子模塊。
- git submodule update --recursive --remote :拉取所有子模塊的更新。
Module Federation 是什么?
官方文檔解釋其動機(jī)如下:
多個(gè)獨(dú)立的構(gòu)建可以組成一個(gè)應(yīng)用程序,這些獨(dú)立的構(gòu)建之間不應(yīng)該存在依賴關(guān)系,因此可以單獨(dú)開發(fā)和部署它們。這通常被稱作微前端,但不僅僅限于此。
Module federation 使 JavaScript 應(yīng)用得以從另一個(gè) JavaScript 應(yīng)用中動態(tài)地加載代碼,這就解決了我們上面提到的模塊共享的問題。
它不僅僅是微前端,而且場景粒度可以更加細(xì),一般微前端更多的是應(yīng)用級別,但它更偏向模塊級別的共享。
Module Federation 配置
在實(shí)戰(zhàn)之前,我們了解一下 Module Federation 的配置項(xiàng)
首先是兩個(gè)基礎(chǔ)角色的約定:
- Host。消費(fèi)模塊的一方。
- Remote。提供模塊的一方。
每個(gè)應(yīng)用都既可以作為 host,也可以作為 remote。
Module Federation 配置項(xiàng)如下:
- name: 必須且唯一。
- filename: 若沒有提供 filename,那么構(gòu)建生成的文件名與容器名稱同名。
- remotes: 可選,作為引用方最關(guān)鍵的配置項(xiàng),用于聲明需要引用的遠(yuǎn)程資源包的名稱與模塊名稱,作為 Host 時(shí),去消費(fèi)哪些 Remote。
- exposes: 可選,表示作為 Remote 時(shí),export 哪些屬性被消費(fèi)。
- library: 可選,定義了 remote 應(yīng)用如何將輸出內(nèi)容暴露給 host 應(yīng)用。配置項(xiàng)的值是一個(gè)對象,如 { type: 'xxx', name: 'xxx'}。
- shared,可選,指示 remote 應(yīng)用的輸出內(nèi)容和 host 應(yīng)用可以共用哪些依賴。shared 要想生效,則 host 應(yīng)用和 remote 應(yīng)用的 shared 配置的依賴要一致。
Singleton: 是否開啟單例模式。默認(rèn)值為 false,開啟后remote 應(yīng)用組件和 host 應(yīng)用共享的依賴只加載一次,而且是兩者中版本比較高的。
requiredVersion:指定共享依賴的版本,默認(rèn)值為當(dāng)前應(yīng)用的依賴版本。
eager:共享依賴在打包過程中是否被分離為 async chunk。設(shè)置為 true, 共享依賴會打包到 main、remoteEntry,不會被分離,因此當(dāng)設(shè)置為true時(shí)共享依賴是沒有意義的。
實(shí)戰(zhàn)演示
這里我們用 Github 中 Module Federation Examples[1]進(jìn)行演示。這里包含了基礎(chǔ)的用法、高級用法以及和一些框架的結(jié)合實(shí)踐。
注:該倉庫使用 lerna 維護(hù)。所以你需要安裝 lerna。
npm install lerna -g
通過 lerna bootstrap 安裝依賴。
簡單示例
來看 basic-host-remote 目錄下有兩個(gè)獨(dú)立的 project,分別為 app1 和 app2。其中 app2 中實(shí)現(xiàn)了一個(gè) Button 組件,現(xiàn)在 app1 要用這個(gè) Button 組件。
import React from 'react';
const Button = () => ;
export default Button;
app2 暴露組件
此時(shí),app2 的角色就是 Remote,核心 webpack 配置:
const { ModuleFederationPlugin } = require('webpack').container;
// ...
plugins: [
new ModuleFederationPlugin({
name: 'app2',
library: { type: 'var', name: 'app2' },
filename: 'remoteEntry.js', // 生成的文件名
exposes: {
'./Button': './src/Button', // Export Button 組件
},
// 共享 react 和 react-dom
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
// ...app1 消費(fèi)組件
此時(shí),app1 的角色是 Host,webpack 核心配置:
const { ModuleFederationPlugin } = require('webpack').container;
// ...
//http://localhost:3002/remoteEntry.js
plugins: [
new ModuleFederationPlugin({
name: 'app1',
remotes: {
// http://localhost:3002/remoteEntry.js
// 上面配置生成的模塊文件
app2: `app2@${getRemoteEntryUrl(3002)}`,
},
// 共享模塊
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
],
// ...模塊使用:
const RemoteButton = React.lazy(() => import('app2/Button'));
const App = () => (
Basic Host-Remote
App 1
);
export default App;效果
而且可以看到 react 和 react-dom 也是加載了一次:
高級示例-動態(tài)加載遠(yuǎn)程模塊
假如初始化的時(shí)候,不加載遠(yuǎn)程的模塊,在一定的交互之后再去加載遠(yuǎn)程模塊,該怎么實(shí)現(xiàn)呢?
本示例在 advanced-api/dynamic-remotes 中可以找到。
示例中有三個(gè) project,app1/app2/app3。app1 是 Host,消費(fèi) app2 和 app3 提供的組件,而且點(diǎn)擊相應(yīng)按鈕的時(shí)候才去加載對應(yīng)的遠(yuǎn)程模塊。另外 app2 和 app3 都用到了 moment.js。
app2 和 app3 暴露模塊
兩個(gè) project 的配置是相似的,都是暴露了 Widget 組件,而且都同享了 react 和 react-dom,以及 moment.js。這里可以留意的是,假如不聲明 requiredVersion,就會使用它能找到的當(dāng)前大版本中最高的 version。
const deps = require('./package.json').dependencies;
// ...
new ModuleFederationPlugin({
name: 'app3',
library: { type: 'var', name: 'app3' },
filename: 'remoteEntry.js',
exposes: {
'./Widget': './src/Widget',
},
// adds react as shared module
// version is inferred from package.json
// there is no version check for the required version
// so it will always use the higher version found
shared: {
react: {
requiredVersion: deps.react,
import: 'react', // the "react" package will be used a provided and fallback module
shareKey: 'react', // under this name the shared module will be placed in the share scope
shareScope: 'default', // share scope with this name will be used
singleton: true, // only a single version of the shared module is allowed
},
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true, // only a single version of the shared module is allowed
},
// adds moment as shared module
// version is inferred from package.json
// it will use the highest moment version that is>=2.24and 小于 3
moment: deps.moment,
},
})app1 消費(fèi)模塊
app1 作為 Host,這里都是常規(guī)配置,不再贅述。
主要來看它負(fù)責(zé)動態(tài)加載的代碼,在點(diǎn)擊相應(yīng)的按鈕的時(shí)候,會觸發(fā) useFederatedComponent 方法,入?yún)⒅?remoteUrl 為遠(yuǎn)程地址,scope 為對應(yīng)應(yīng)用名稱,module 為指定的模塊。其中 useDynamicScript 負(fù)責(zé)加載的遠(yuǎn)程 JavaScript 腳本,加載完成之后,通過 loadComponent 方法動態(tài)加載組件。
export const useFederatedComponent = (remoteUrl, scope, module) => {
const key = `${remoteUrl}-${scope}-${module}`;
const [Component, setComponent] = React.useState(null);
const { ready, errorLoading } = useDynamicScript(remoteUrl);
React.useEffect(() => {
if (Component) setComponent(null);
// Only recalculate when key changes
}, [key]);
React.useEffect(() => {
if (ready && !Component) {
const Comp = React.lazy(loadComponent(scope, module));
componentCache.set(key, Comp);
setComponent(Comp);
}
// key includes all dependencies (scope/module)
}, [Component, ready, key]);
return { errorLoading, Component };
};再來重點(diǎn)看下 loadComponent,其中 __webpack_init_sharing__ ,進(jìn)行了初始化共享作用域,用提供的已知此構(gòu)建和所有遠(yuǎn)程的模塊填充它。然后獲得遠(yuǎn)程容器 container,支持 get 和 init 方法。init 是一個(gè)兼容 async 的方法,調(diào)用時(shí),只含有一個(gè)參數(shù):共享作用域?qū)ο?shared scope object)——__webpack_share_scopes__.default。最后調(diào)用容器的 get 方法,獲取到對應(yīng)的模塊。
function loadComponent(scope, module) {
return async () => {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
// 初始化共享作用域(shared scope)用提供的已知此構(gòu)建和所有遠(yuǎn)程的模塊填充它
await __webpack_init_sharing__('default');
const container = window[scope]; // or get the container somewhere else
// 初始化容器 它可能提供共享模塊
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
}效果演示
- 點(diǎn)擊不同的按鈕,加載不同的組件。
- moment.js 在首次加載后不用再重新加載。
你可以通過動態(tài)加載的方式,提供一個(gè)共享模塊的不同版本,從而實(shí)現(xiàn) A/B 測試。
Module Federation 的問題
談了這么多 Module Federation 的優(yōu)點(diǎn),我們來看看它有哪些缺點(diǎn)
- 對環(huán)境要求略高,需要使用 webpack5,舊項(xiàng)目改造成本大。
- 拆分粒度需要權(quán)衡,雖然能做到依賴共享,但是被共享的 lib 不能做 tree-shaking,也就是說如果共享了一個(gè) lodash,那么整個(gè) lodash 庫都會被打包到 shared-chunk 中。
- Webpack 為了支持加載 remote 模塊對 runtime 做了大量改造,在運(yùn)行時(shí)要做的事情也因此陡然增加,可能會對我們頁面的運(yùn)行時(shí)性能造成負(fù)面影響。
- 運(yùn)行時(shí)共享也是一把雙刃劍,如何去做版本控制以及控制共享模塊的影響是需要去考慮的問題。
對于問題1,未來應(yīng)該會慢慢變好。問題2 感覺還好,場景應(yīng)該不會特別多,而且相比于共享模塊,不重復(fù)編譯的優(yōu)點(diǎn)來講,相對可以接受。問題3,感受不大。
問題4,算是比較頭疼的一件事,比如幾個(gè)項(xiàng)目,都需要版本 react/react-dom/antd 的版本一致,假如版本更新的話,怎么辦?
我們可以使用 Module Federation 的能力,將一些核心的依賴?yán)?react、react-dom、antd,使用一個(gè) remote 服務(wù)維護(hù),然后每個(gè)項(xiàng)目分別引用這個(gè)服務(wù)導(dǎo)出的 library。我們只需要維護(hù)這個(gè) remote 服務(wù)上依賴的版本,就能保證每個(gè)項(xiàng)目核心依賴的版本是一致的,而且升級的時(shí)候,也不用每個(gè)項(xiàng)目自己升級,大大提升了效率。
總結(jié)
使用 Module Federation,我們可以在一個(gè)應(yīng)用中動態(tài)加載并執(zhí)行另一個(gè)應(yīng)用的代碼,且與技術(shù)棧無關(guān),并且能夠共享模塊,從而減小編譯時(shí)間以及降低包體積。
但在使用 Module Federation 的時(shí)候也需要權(quán)衡模塊拆分的粒度以及做好版本的控制。
參考
- 深入探索Webpack5之Module Federation的“奇淫技巧”[2]
- 官網(wǎng) Module Federation[3]
- 淺析 Webpack Module Federation 在 React.js 中的實(shí)踐[4]
參考資料
[1]Module Federation Examples: https://github.com/module-federation/module-federation-examples
[2]深入探索Webpack5之Module Federation的“奇淫技巧”: https://juejin.cn/post/6938975818659921957
[3]官網(wǎng) Module Federation: https://webpack.docschina.org/concepts/module-federation/
[4]淺析 Webpack Module Federation 在 React.js 中的實(shí)踐: https://juejin.cn/post/7012990703714172964
新聞名稱:探索Webpack5中的ModuleFederation
鏈接URL:http://www.dlmjj.cn/article/dhijcee.html


咨詢
建站咨詢
