Module federation讓一個(gè)JS APP能夠在server端或者client端運(yùn)行來(lái)自其他
bundle/build中的代碼。
背景
共享code/組件一直是一個(gè)永恒的話(huà)題。
在同一APP中想要復(fù)用/共享一段代碼很簡(jiǎn)單,可以提出一個(gè)function,如果是UI級(jí)別的復(fù)用,那么就是提出一個(gè)組件,通過(guò)模塊的import/export實(shí)現(xiàn)。
但是在不同的APP中想要共享/復(fù)用一段代碼/組件就沒(méi)有那么簡(jiǎn)單了,通常都只是通過(guò)提出一個(gè)npm包,放在npm register中統(tǒng)一管理,不同的APP如果想要使用,就從npm register安裝。如果遇到版本升級(jí),所有使用該庫(kù)的APP都需要手動(dòng)進(jìn)行升級(jí),依然不是很方便。
What is Module Federation?
Module Federation是webpack5中的新功能,這個(gè)功能能夠讓一個(gè)JS APP動(dòng)態(tài)的從另外一個(gè)JS APP加載代碼,利用最小的花費(fèi)實(shí)現(xiàn)代碼的復(fù)用。
從另一個(gè)JS APP中加載來(lái)的代碼通常被叫做federated module code。
加載來(lái)的代碼還能夠直接使用當(dāng)前APP中的shared dependency作為自己的dependency, 如果當(dāng)前APPshared dependencies中并沒(méi)有其所需的dependency,那么webpack會(huì)從federated module code所在的APP中下載。
Terminology
Module federation: 在多APP(瀏覽器/nodejs)中實(shí)現(xiàn)動(dòng)態(tài)地代碼共享,A host: 其實(shí)就是一個(gè)可以自己運(yùn)行的APP,但是會(huì)作為消費(fèi)者,從其他的APP中請(qǐng)求federated module code。A remote: 其實(shí)是另一個(gè)APP, 但是會(huì)提供federated module codeBidirectional-hosts: 即可以自己運(yùn)行作為host消費(fèi)其他APP的federated module code,也可以作為remote為其他的APP提供federated module code。
SPA Bidirectional-hosts
假設(shè)我們計(jì)劃做一個(gè)SPA, 這個(gè)APP可能有三個(gè)頁(yè)面:
- Dashboard頁(yè)面
- SRP頁(yè)面
- PDP頁(yè)面
我們期待這個(gè)三個(gè)頁(yè)面可以獨(dú)立開(kāi)發(fā),獨(dú)立部署,獨(dú)立運(yùn)行, 那么webpack federation就是一個(gè)很好的工具。
每一個(gè)頁(yè)面都是一個(gè)單獨(dú)的APP,有獨(dú)立的repo,獨(dú)立開(kāi)發(fā),獨(dú)立部署,獨(dú)立運(yùn)行。
因此每一個(gè)頁(yè)面,當(dāng)然也是每一個(gè)APP都是bi-directional hosts.
任何一個(gè)頁(yè)面(APP),如果先打開(kāi)(先load)那么就是host, 如果要跳轉(zhuǎn)到其他頁(yè)面,也能夠利用react router,做一個(gè)動(dòng)態(tài)import從其他page(APP)中,把相應(yīng)頁(yè)面的組件加載過(guò)來(lái)。此時(shí)在新的頁(yè)面上刷新,那么當(dāng)前頁(yè)面所在的APP就會(huì)被load,此時(shí)當(dāng)前頁(yè)面所在的APP就變成了host。
這樣,由于每一個(gè)page(APP)
- 都使用react router成為了一個(gè)SPA,因此可以作為host
- 都使用webpack federation module,將當(dāng)前頁(yè)面的根組件作為federated code暴露出去,因此也可以作為remote。
這樣,如果你了解過(guò)Route級(jí)別的code split,那么頁(yè)面之間的跳轉(zhuǎn)就不再需要external跳轉(zhuǎn),就只需要使用動(dòng)態(tài)import,從remote將新頁(yè)面的組件取回即可。
如何處理federated component的dependency
加入App A暴露了一個(gè)APP component, 這時(shí)候APP B作為消費(fèi)者會(huì)使用APP component,但是APP component 可能依賴(lài)于
"react", "react-dom" 。
那我們的處理方案是否和一般的npm package一樣,將該package需要使用的所有依賴(lài)都包含在這個(gè)npm package中?
這樣做會(huì)出現(xiàn)的問(wèn)題:
- 包非常的大
- 如果npm package需要依賴(lài)react,如果其將react打入到了最終的npm包中,如果APP B也是一個(gè)react app,那么極有可能出現(xiàn)APP B中有兩React,這個(gè)是絕對(duì)不能被React 允許的
這種時(shí)候?qū)τ贜PM包我們會(huì)想到使用peer dependency,對(duì)就是直接讓包使用宿主中的依賴(lài)。
同理webpack federation也提供了shared字段,也就是明確寫(xiě)明,當(dāng)前的repo中能夠用于和remote共享的依賴(lài)是那些。
這樣從其他APP引入的component可以直接使用宿主的shared dependency,盡可能減少code duplication
實(shí)例
APP1
- APP1中將會(huì)一個(gè)根組件
APPContainer, 這個(gè)組件可以被其他APP消費(fèi) - APP1也是一個(gè)SPA, 可以獨(dú)立運(yùn)行,也可以作為消費(fèi)者消費(fèi)其他APP的組件
- 由于APP1會(huì)做消費(fèi)者(host),因此會(huì)和federated Module共享依賴(lài)
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
// other webpack configs...
plugins: [
new ModuleFederationPlugin({
name: '_app_one_remote', // 當(dāng)前APP作為remote暴露組件時(shí)的APP的名字
library: 'app_one_remote', // 當(dāng)前APP作為remote暴露組件時(shí)的library名字
filename: 'remoteEntry.js',
// 所有被暴露的組件會(huì)打包到這個(gè)文件中,同理使用者也需要從這里引入
remotes: {
app_two: "app_two_remote",
app_three: "app_three_remote"
}, // 定義該庫(kù)作為host時(shí)可能要引用的remote
exposes: {
'AppContainer': './src/App'
}, // 定義該庫(kù)作為remote時(shí),要暴露出去的組件。左邊是相對(duì)路徑和組件名字(其他庫(kù)使用時(shí)候),右邊是該組件在本庫(kù)內(nèi)的路徑
shared: ["react", "react-dom","react-router-dom"]// 和引入的組件公用的dependency
})
]
};
那么當(dāng)APP1作為remote的時(shí)候,其他的庫(kù)是如何使用APP1暴露出去的組件APPContainer呢?很簡(jiǎn)單、
APP1已經(jīng)被部署和獨(dú)立運(yùn)行后,由于webpack的構(gòu)建,該APP會(huì)有不同的入口和bundle。
- 正常的入口是index.js保證APP的運(yùn)行
- 另外一個(gè)入口是暴露出去的組件
因此一旦這些打出來(lái)的靜態(tài)文件被服務(wù)器server,比如服務(wù)運(yùn)行在localhost:3000,
- 你可以通過(guò)訪(fǎng)問(wèn)
http://localhost:3000看到APP正常運(yùn)行 - 也可以訪(fǎng)問(wèn)
http://localhost:3000/app_one_remote.js獲取到該APP暴露出來(lái)的組件。
作為consumer你可以這樣引入來(lái)自APP1的組件
- 配置webpack,使其可以順利的找到remote的組件
const ModuleFederationPlugin = require('webpack-plugin-module-federation');
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: '_app_two_remote',
library: 'app_two_remote',
filename: 'remoteEntry.js',
libraryTarget: 'global',
remotes: {
'app_one_remote': '_app_one_remote'
},
expose: {
App: './src/App'
},
}),
]
};
- 首先需要在HTML中引入remote的組件包文件
<head>
<script src="http://localhost:3001/remoteEntry.js"></script>
</head>
<body>
<div id="root"></div>
</body>
- 動(dòng)態(tài)import將要使用的組件從HTML中加載回來(lái)的remoteEntry.js文件中引入
import React, { lazy, Suspense, useState } from 'react';
import Footer from './Footer';
const AppContainer = lazy(() => import('app_one_remote/AppContainer')); // federated
export default () => {
return (
<>
<Suspense fallback={'fallback'}>
<AppContainer />
</Suspense>
<p>
// ....
</p>
<Footer />
</>
);
};
使用場(chǎng)景
- Domain
通常對(duì)于一個(gè)可能涉及多個(gè)不同sub site的網(wǎng)站,比如舉個(gè)例子房產(chǎn)網(wǎng)站,類(lèi)似于安居客之類(lèi)的。這樣的網(wǎng)站大多會(huì)涉及多種不同的業(yè)務(wù),比如二手房屋買(mǎi)賣(mài),新房買(mǎi)賣(mài),房屋出租等,這些不同的領(lǐng)域通常由不同的組來(lái)維護(hù),最適合使用webpack module federation做成micro frontend。
不同的組獨(dú)立開(kāi)發(fā)獨(dú)立部署自己的網(wǎng)站,同時(shí):
- 將自己的網(wǎng)站的APP組件暴露出來(lái)
- 能夠動(dòng)態(tài)引入其他網(wǎng)站的APP組件

- Widget
專(zhuān)門(mén)獨(dú)立部署一個(gè)APP,其中包含多個(gè)不同的widget,或者UI組件專(zhuān)門(mén)供給不同的APP
