詳解 Module Federation 的實(shí)現(xiàn)原理
大廠技術(shù) 高級(jí)前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
作者:@西陵
原文:https://juejin.cn/post/7151281452716392462
基本概念
1、什么是 Module Federation?
首先看一下官方給出的解釋:
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.
簡(jiǎn)單理解就是說(shuō) “一個(gè)應(yīng)用可以由多個(gè)獨(dú)立的構(gòu)建組成,這些構(gòu)建彼此獨(dú)立沒(méi)有依賴關(guān)系,他們可以獨(dú)立開發(fā)、部署。這就是常被認(rèn)為的微前端,但不局限于此”

MF 解決的問(wèn)題其實(shí)和微前端有些類似,都是將一個(gè)應(yīng)用拆分成多個(gè)子應(yīng)用,每個(gè)應(yīng)用都可以獨(dú)立開發(fā)、部署,但是他們也有一些區(qū)別,比如微前端需要一個(gè)中心應(yīng)用(簡(jiǎn)稱基座)去承載子應(yīng)用,而 MF 不需要,因?yàn)槿魏我粋€(gè)應(yīng)用都可以作為中心應(yīng)用,其次就是 MF 可以實(shí)現(xiàn)應(yīng)用之間的依賴共享。
2、Module Federation 核心概念
Container
一個(gè)使用 ModuleFederationPlugin 構(gòu)建的應(yīng)用就是一個(gè) Container,它可以加載其他的 Container,也可以被其他的 Container 加載。
Host&Remote
從消費(fèi)者和生產(chǎn)者的角度看 Container,Container 可以分為 Host 和 Remote,Host 作為消費(fèi)者,他可以動(dòng)態(tài)加載并運(yùn)行其他 Remote 的代碼,Remote 作為提供方,他可以暴露出一些屬性、方法或組件供 Host 使用,這里要注意的一點(diǎn)是一個(gè) Container 既可以作為 Host 也可以作為 Remote。
Shared
shared 表示共享依賴,一個(gè)應(yīng)用可以將自己的依賴共享出去,比如 react、react-dom、mobx 等,其他的應(yīng)用可以直接使用共享作用域中的依賴從而減少應(yīng)用的體積。
3、使用案例
下面通過(guò)一個(gè)實(shí)例來(lái)演示一下 MF 的功能,該項(xiàng)目由 main 和 component 兩個(gè)應(yīng)用組成,component 應(yīng)用會(huì)將自己的組件 exposes 出去,并將 react 和 react-dom 共享出來(lái)給 main 應(yīng)用使用。
完成代碼可查看這里 https://github.com/projectcss/react-mf
大家最好將源代碼下載下來(lái)自己跑一遍便于理解,下面展示的是 main 應(yīng)用的代碼,在 App 組件中我們引入了 component 應(yīng)用的 Button、Dialog 和 ToolTip 組件。
main/src/App.js
import React, {useState} from 'react';
import Button from 'component-app/Button';
import Dialog from 'component-app/Dialog';
import ToolTip from 'component-app/ToolTip';
const App = () => {
const [dialogVisible, setDialogVisible] = useState(false);
const handleClick = (ev) => {
setDialogVisible(true);
}
const handleSwitchVisible = (visible) => {
setDialogVisible(visible);
}
return (
<div>
<h1>Open Dev Tool And Focus On Network,checkout resources details</h1>
<p>
components hosted on <strong>component-app</strong>
</p>
<h4>Buttons:</h4>
<Button type="primary" />
<Button type="warning" />
<h4>Dialog:</h4>
<button onClick={handleClick}>click me to open Dialog</button>
<Dialog switchVisible={handleSwitchVisible} visible={dialogVisible} />
<h4>hover me please!</h4>
<ToolTip content="hover me please" message="Hello,world!" />
</div>
);
}
export default App;
效果如下:

我們看到,因?yàn)?main 應(yīng)用 引用了 component 應(yīng)用的組件,所以在渲染的時(shí)候需要異步去下載 component 應(yīng)用的入口代碼(remoteEntry)以及組件,同時(shí)只下載了 main 應(yīng)用共享出去的 react 和 react-dom 這兩個(gè)依賴,也就是說(shuō) component 中的組件使用的就是 main 應(yīng)用 提供的依賴,這樣就實(shí)現(xiàn)了代碼動(dòng)態(tài)加載以及依賴共享的功能。
4、插件配置
為了實(shí)現(xiàn)聯(lián)邦模塊的功能,webpack 接住了一個(gè)插件 ModuleFederationPlugin,下面我們就拿上面的例子來(lái)介紹插件的配置。
component/webpack.config.js
const {ModuleFederationPlugin} = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './index.js',
// ...
plugins: [
new ModuleFederationPlugin({
name: 'component_app',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button.jsx',
'./Dialog': './src/Dialog.jsx',
'./Logo': './src/Logo.jsx',
'./ToolTip': './src/ToolTip.jsx',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})
],
};
作為提供方,component 將自己的 Button、Dialog 等組件暴露出去,同時(shí)將 react 和 react-dom 這兩個(gè)依賴共享出去。
main/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
entry: './index.js',
// ...
plugins: [
new ModuleFederationPlugin({
name: 'main_app',
remotes: {
'component-app': 'component_app@http://localhost:3001/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})
],
};
作為消費(fèi)者的 main 應(yīng)用需要定義需要消費(fèi)的應(yīng)用的名稱以及地址,同時(shí) main 應(yīng)用也將自己的 react 和 react-dom 這兩個(gè)依賴共享出去。
下面來(lái)介紹幾個(gè)核心的配置字段:
name
name 表示當(dāng)前應(yīng)用的別名,當(dāng)作為 remote 時(shí)被 host 引用時(shí)需要在路徑前加上這個(gè)前綴,比如 main 中的 remote 配置:
remotes: {
'component-app': 'component_app@http://localhost:3001/remoteEntry.js',
},
路徑的前綴 component_app 就是 component 應(yīng)用的 name 值。
filename
filename 表示 remote 應(yīng)用提供給 host 應(yīng)用使用時(shí)的入口文件,比如上面 component 應(yīng)用設(shè)置的是 remoteEntry,那么在最終的構(gòu)建產(chǎn)物中就會(huì)出現(xiàn)一個(gè) remoteEntry.js 的入口文件供 main 應(yīng)用加載。
exposes
exposes 表示 remote 應(yīng)用有哪些屬性、方法和組件需要暴露給 host 應(yīng)用使用,他是一個(gè)對(duì)象,其中 key 表示在被 host 使用的時(shí)候的相對(duì)路徑,value 則是當(dāng)前應(yīng)用暴露出的屬性的相對(duì)路徑,比如在引入 Button 組件時(shí)可以這么寫:
import Button from 'component-app/Button';
remote
remote 表示當(dāng)前 host 應(yīng)用需要消費(fèi)的 remote 應(yīng)用的以及他的地址,他是一個(gè)對(duì)象,key 為對(duì)應(yīng) remote 應(yīng)用的 name 值,這里要注意這個(gè) name 不是 remote 應(yīng)用中配置的 name,而是自己為該 remote 應(yīng)用自定義的值,value 則是 remote 應(yīng)用的資源地址。
shared
當(dāng)前應(yīng)用無(wú)論是作為 host 還是 remote 都可以共享依賴,而共享的這些依賴需要通過(guò) shared 去指定。
new ModuleFederationPlugin({
name: 'main_app',
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})
他的配置方式有三種,具體可以查看官網(wǎng),這里只介紹常用的對(duì)象配置形式,在對(duì)象中 key 表示第三方依賴的名稱,value 則是配置項(xiàng),常用的配置項(xiàng)有 singleton 和 requiredVersion。
singleton: 表示是否開啟單例模式,如果開啟的話,共享的依賴則只會(huì)加載一次(優(yōu)先取版本高的)。
requiredVersion: 表示指定共享依賴的版本。
比如 singleton 為 true 時(shí),main 的 react 版本為 16.14.0,component 的 react 版本為 16.13.0,那么 main 和 component 將會(huì)共同使用 16.14.0 的 react 版本,也就是 main 提供的 react。
如果這時(shí) component 的配置中將 react 的 requiredVersion 設(shè)置為 16.13.0,那么 component 將會(huì)使用 16.13.0,main 將會(huì)使用 16.14.0,相當(dāng)于它們都沒(méi)有共享依賴,各自下載自己的 react 版本。
工作原理
1、使用 MF 后在構(gòu)建上有什么不同?
在沒(méi)有使用 MF 之前,component,lib 和 main 的構(gòu)建如下:

使用 MF 之后構(gòu)建結(jié)果如下:

對(duì)比上面兩張圖我們可以看出使用 MF 構(gòu)建出的產(chǎn)物發(fā)生了變化,里面新增了 remoteEntry-chunk、shared-chunk、expose-chunk 以及 async-chunk。
其中 remoteEntry-chunk、shared-chunk 和 expose-chunk 是因?yàn)槭褂昧?ModuleFederationPlugin 而生成的,async-chunk 是因?yàn)槲覀兪褂昧水惒綄?dǎo)入 import() 而產(chǎn)生的。
下面我們對(duì)照著 component 的插件配置介紹一下每個(gè) chunk 的生成。
component/wenpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
entry: './index.js',
// ....
plugins: [
new ModuleFederationPlugin({
name: 'component_app',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button.jsx',
'./Dialog': './src/Dialog.jsx',
'./Logo': './src/Logo.jsx',
'./ToolTip': './src/ToolTip.jsx',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})
]
};
remoteEntry-chunk 是當(dāng)前應(yīng)用作為遠(yuǎn)程應(yīng)用 (Remote) 被調(diào)用的時(shí)候請(qǐng)求的文件,對(duì)應(yīng)的文件名為插件里配置的 filename,我們當(dāng)前設(shè)置的名稱就叫做 remoteEntry.js,我們可以打開 main 應(yīng)用的控制臺(tái)查看:

shared-chunk 是當(dāng)前應(yīng)用開啟了 shared 功能后生成的,比如我們?cè)?shared 中指定了 react 和 react-dom,那么在構(gòu)建的時(shí)候 react 和 react-dom 就會(huì)被分離成新的 shared-chunk,比如
vendors-node_modules_react_index_js.js和vendors-node_modules_react-dom_index_js.js。espose-chunk 是當(dāng)前應(yīng)用暴露一些屬性 / 組件給外部使用的時(shí)候生成的,在構(gòu)建的時(shí)候會(huì)根據(jù) exposes 配置項(xiàng)生成一個(gè)或多個(gè) expose-chunk,比如
src_Button_jsx.js、src_Dialog_jsx.js和src_ToolTip_jsx.js。async-chunk 是一個(gè)異步文件,這里指的其實(shí)就是
bootstrap_js.js,為什么需要生成一個(gè)異步文件呢?我們看看 main 應(yīng)用中的bootstrap.js和index.js文件:
main/src/bootstrap.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
ReactDOM.render(<App />, document.getElementById('app'));
main/src/index.js
import('./bootstrap')
一般在我們的項(xiàng)目中 index.js 作為我們的入口文件里面應(yīng)該存放的是 bootstrap.js 中的代碼,這里卻將代碼單獨(dú)抽離出來(lái)放到 bootstrap.js 中,同時(shí)在 index.js 中使用 import('./bootstrap') 來(lái)異步加載 bootstrap.js,這是為什么呢?
我們來(lái)看下這段代碼:
main/src/App.js
import React, {useState} from 'react';
import Button from 'component-app/Button';
const App = () => {
return (
<div>
<Button type="primary" />
</div>
);
}
export default App;
如果 bootstrap.js 不是異步加載的話而是直接打包在 main.js 里面,那么 import Button from 'component-app/Button 就會(huì)被立即執(zhí)行了,但是此時(shí) component 的資源根本沒(méi)有被下載下來(lái),所以就會(huì)報(bào)錯(cuò)。

如果我們開啟了 shared 功能的話,那么 import React from 'react' 這句被同步執(zhí)行也會(huì)報(bào)錯(cuò),因?yàn)檫@時(shí)候還沒(méi)有初始化好共享的依賴。

所以必須把原來(lái)的入口代碼放到 bootstrap.js 里面,在 index.js 中使用 import 來(lái)異步加載 bootstrap.js ,這樣可以實(shí)現(xiàn)先加載 main.js,然后在異步加載 bootstrap_js.js(async-chunk) 的時(shí)候先加載好遠(yuǎn)程應(yīng)用的資源并初始化好共享的依賴,最后再執(zhí)行 bootstrap.js 模塊。
2、如何加載遠(yuǎn)程模塊?
我們先看一下 webpack 是怎么轉(zhuǎn)換 main 應(yīng)用中的導(dǎo)入語(yǔ)句:main/src/App.js
import React, {useState} from 'react';
import Button from 'component-app/Button';
import Dialog from 'component-app/Dialog';
import ToolTip from 'component-app/ToolTip';
const App = () => {
return (
<div>
<Button type="primary" />
</div>
);
}
export default App;
在 bootstrap_js.js 中找到編譯后的結(jié)果:

我們可以看到 component-app/Button 最終會(huì)被編譯成 webpack/container/remote/component-app/Button,但是 webpack/container/remote/component-app/Button 又在哪呢,我們從 main 應(yīng)用的 入口文件 main.js 中可以查找到:
(() => {
var chunkMapping = {
"bootstrap_js": [
"webpack/container/remote/component-app/Button",
"webpack/container/remote/component-app/Dialog",
"webpack/container/remote/component-app/ToolTip"
]
};
var idToExternalAndNameMapping = {
"webpack/container/remote/component-app/Button": [
"default",
"./Button",
"webpack/container/reference/component-app"
],
"webpack/container/remote/component-app/Dialog": [
"default",
"./Dialog",
"webpack/container/reference/component-app"
],
"webpack/container/remote/component-app/ToolTip": [
"default",
"./ToolTip",
"webpack/container/reference/component-app"
]
};
__webpack_require__.f.remotes = (chunkId, promises) => {
if(__webpack_require__.o(chunkMapping, chunkId)) {
chunkMapping[chunkId].forEach((id) => {
var getScope = __webpack_require__.R;
if(!getScope) getScope = [];
var data = idToExternalAndNameMapping[id];
if(getScope.indexOf(data) >= 0) return;
getScope.push(data);
if(data.p) return promises.push(data.p);
var onError = (error) => {
if(!error) error = new Error("Container missing");
if(typeof error.message === "string")
error.message += '\nwhile loading "' + data[1] + '" from ' + data[2];
__webpack_require__.m[id] = () => {
throw error;
}
data.p = 0;
};
var handleFunction = (fn, arg1, arg2, d, next, first) => {
try {
var promise = fn(arg1, arg2);
if(promise && promise.then) {
var p = promise.then((result) => (next(result, d)), onError);
if(first) promises.push(data.p = p); else return p;
} else {
return next(promise, d, first);
}
} catch(error) {
onError(error);
}
}
var onExternal = (external, _, first) => (external ? handleFunction(__webpack_require__.I, data[0], 0, external, onInitialized, first) :
onError());
var onInitialized = (_, external, first) => (handleFunction(external.get, data[1], getScope, 0, onFactory, first));
var onFactory = (factory) => {
data.p = 1;
__webpack_require__.m[id] = (module) => {
module.exports = factory();
}哪及了
};
handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
});
}
}
})();
這里的 __webpack_require__.f.remotes 就是加載遠(yuǎn)程模塊的核心代碼,代碼中有個(gè) chunkMapping 對(duì)象,這個(gè)對(duì)象保存的是當(dāng)前應(yīng)用的那些模塊依賴了遠(yuǎn)程應(yīng)用,idToExternalAndNameMapping 對(duì)象保存的是被依賴的遠(yuǎn)程模塊的基本信息,便于后面遠(yuǎn)程請(qǐng)求該模塊。
在加載 bootstrap_js.js 的時(shí)候必須先加載完遠(yuǎn)程應(yīng)用的資源,對(duì)于我們的例子來(lái)說(shuō)如果我們想要使用遠(yuǎn)程應(yīng)用中的 Button、Tooltip 組件就必須先加載這個(gè)應(yīng)用的資源,即 webpack/container/reference/component-app,這個(gè)從 handleFunction 方法中就可以看出來(lái),data[2] 也就代表著 idToExternalAndNameMapping 中每一項(xiàng)對(duì)應(yīng)的數(shù)組的第二項(xiàng)數(shù)據(jù),下面我們?cè)?nbsp;main.js 中找到 webpack/container/reference/component-app:

這里會(huì)異步去加載 component 的 remoteEntry.js,也就是我們?cè)?main 應(yīng)用中配置 ModuleFederationPlugin 的時(shí)候制定的 component 遠(yuǎn)程模塊的入口文件的資源地址,加載完后返回 componnet_app 這個(gè)全局變量作為 webpack/container/reference/component-app 模塊的輸出值,這里有兩個(gè)點(diǎn)要注意:
這里是通過(guò) JSONP 的形式去加載遠(yuǎn)程應(yīng)用,拿到遠(yuǎn)程應(yīng)用的
remoteEntry.js文件后再去執(zhí)行。componnet_app 是 入口文件
remoteEntry.js中的一個(gè)全局變量,再執(zhí)行該文件的時(shí)候會(huì)往這個(gè)全局變量上掛載屬性,這個(gè)后面會(huì)介紹。

但是這里我們只是獲得了 componnet_app 這個(gè)遠(yuǎn)程模塊的輸出值,但是怎么獲取到 Button、Tooltip 組件呢?
我們先來(lái)看一下 component 的 remoteEntry.js 文件:
// 組件和地址的映射表
var moduleMap = {
"./Button": () => {
return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_react_react-_185b"), __webpack_require__.e("src_Button_jsx")]).then(() => (()
=> ((__webpack_require__(/*! ./src/Button.jsx */ "./src/Button.jsx")))));
},
"./Dialog": () => {
return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_react_react-_185b"), __webpack_require__.e("src_Dialog_jsx")]).then(() => (()
=> ((__webpack_require__(/*! ./src/Dialog.jsx */ "./src/Dialog.jsx")))));
},
"./Logo": () => {
return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_react_react-_185b"), __webpack_require__.e("src_Logo_jsx")]).then(() => (()
=> ((__webpack_require__(/*! ./src/Logo.jsx */ "./src/Logo.jsx")))));
},
"./ToolTip": () => {
return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_react_react-_185b"), __webpack_require__.e("src_ToolTip_jsx")]).then(() =>
(() => ((__webpack_require__(/*! ./src/ToolTip.jsx */ "./src/ToolTip.jsx")))));
}
};
// 獲取指定模塊
var get = (module, getScope) => {
__webpack_require__.R = getScope;
getScope = (
__webpack_require__.o(moduleMap, module)
? moduleMap[module]()
: Promise.resolve().then(() => {
throw new Error('Module "' + module + '" does not exist in container.');
})
);
__webpack_require__.R = undefined;
return getScope;
};
var init = (shareScope, initScope) => {
// ...
};
// 往全局變量 component_app 上掛載get和init方法
__webpack_require__.d(exports, {
get: () => (get),
init: () => (init)
});
在 remoteEntry.js 中暴露了 get 和 init 方法,我們回到 main 應(yīng)用的入口文件 main.js ,在 __webpack_require__.f.remotes 里有一個(gè)方法:
var onInitialized = (_, external, first) => (handleFunction(external.get, data[1], getScope, 0, onFactory, first));
這里 external.get 其實(shí)就是 componnet_app.get 方法,data[1] 就是我們的要加載的組件,比如執(zhí)行 componnet_app.get('./Button') 就可以異步獲取 Button 組件。
下面總結(jié)一下整個(gè)流程,main 應(yīng)用首先會(huì)去執(zhí)行入口文件 main.js,然后加載 bootstrap_js 模塊,判斷他依賴了遠(yuǎn)程模塊 webpack/container/remote/component-app/Button,...,那么先會(huì)去下載遠(yuǎn)程模塊 webpack/container/remote/component-app,即 remoteEntry.js ,然后返回 component_app 這個(gè)全局變量,然后執(zhí)行 component-app.get('./xxx') 去獲取對(duì)應(yīng)的組件,等遠(yuǎn)程應(yīng)用的資源以及 bootstrap_js 資源全部下載完成后再執(zhí)行 bootstrap.js 模塊。
3、如何共享依賴?
在 webpack 的構(gòu)建中每個(gè)構(gòu)建產(chǎn)物之間都是隔離的,而要實(shí)現(xiàn)依賴共享就需要打破這個(gè)隔離,這里的關(guān)鍵在于 sharedScope(共享作用域),我們需要在 Host 和 Remote 應(yīng)用之間建立一個(gè)可共享的 sharedScope,里面包含了所有可共享的依賴,之后都按照一定的規(guī)則從這個(gè)共享作用域中獲取相應(yīng)的依賴。

為了探究 webpack 到底是怎么實(shí)現(xiàn)依賴共享的,我們首先看 main 應(yīng)用的入口文件 main.js:
// 共享模塊與對(duì)應(yīng)加載地址映射
var moduleToHandlerMapping = {
"webpack/sharing/consume/default/react/react?ad16": () => (loadSingletonVersionCheckFallback("default", "react", [4,16,14,0], () =>
(Promise.all([__webpack_require__.e("vendors-node_modules_react_index_js"),
__webpack_require__.e("node_modules_object-assign_index_js-node_modules_prop-types_checkPropTypes_js")]).then(() => (() => (__webpack_require__(/*! react */
"./node_modules/react/index.js"))))))),
"webpack/sharing/consume/default/react-dom/react-dom": () => (loadSingletonVersionCheckFallback("default", "react-dom", [4,16,14,0], () =>
(Promise.all([__webpack_require__.e("vendors-node_modules_react-dom_index_js"), __webpack_require__.e("webpack_sharing_consume_default_react_react")]).then(()
=> (() => (__webpack_require__(/*! react-dom */ "./node_modules/react-dom/index.js"))))))),
"webpack/sharing/consume/default/react/react?76b1": () => (loadSingletonVersionCheckFallback("default", "react", [1,16,14,0], () =>
(__webpack_require__.e("vendors-node_modules_react_index_js").then(() => (() => (__webpack_require__(/*! react */ "./node_modules/react/index.js")))))))
};
// 當(dāng)前應(yīng)用依賴的共享模塊
var chunkMapping = {
"bootstrap_js": [
"webpack/sharing/consume/default/react/react?ad16",
"webpack/sharing/consume/default/react-dom/react-dom"
],
"webpack_sharing_consume_default_react_react": [
"webpack/sharing/consume/default/react/react?76b1"
]
};
__webpack_require__.f.consumes = (chunkId, promises) => {
if(__webpack_require__.o(chunkMapping, chunkId)) {
chunkMapping[chunkId].forEach((id) => {
// ...
try {
// 調(diào)用loadSingletonVersionCheckFallback加載共享模塊,
// 并將模塊信息存入共享作用域
var promise = moduleToHandlerMapping[id]();
if(promise.then) {
promises.push(installedModules[id] = promise.then(onFactory)['catch'](onError));
} else onFactory(promise);
} catch(e) { onError(e); }
});
}
}
開啟 shared 功能后會(huì)多了上面這部分邏輯,其中 chunkMapping 這個(gè)對(duì)象保存的是當(dāng)前應(yīng)用有哪些模塊依賴了共享依賴,比如 bootstrap_js 依賴了 react 和 react-dom 這兩個(gè)共享依賴。
那么在加載 bootstrap_js 的時(shí)候就必須先加載完這些共享依賴,這些以來(lái)都是通過(guò) loadSingletonVersionCheckFallback 這個(gè)方法進(jìn)行加載的,下面我們來(lái)看看這個(gè)方法:
var init = (fn) => (function(scopeName, a, b, c) {
var promise = __webpack_require__.I(scopeName);
if (promise && promise.then) return promise.then(fn.bind(fn, scopeName, __webpack_require__.S[scopeName], a, b, c));
return fn(scopeName, __webpack_require__.S[scopeName], a, b, c);
});
var loadSingletonVersionCheckFallback = init((scopeName, scope, key, version, fallback) => {
if(!scope || !__webpack_require__.o(scope, key)) return fallback();
return getSingletonVersion(scope, scopeName, key, version);
});
在執(zhí)行 loadSingletonVersionCheckFallback 之前,首先要執(zhí)行了 init 方法,init 方法中又會(huì)調(diào)用 webpack_require.I ,現(xiàn)在就來(lái)到了共享依賴的重點(diǎn):
(() => {
__webpack_require__.S = {};
var initPromises = {};
var initTokens = {};
__webpack_require__.I = (name, initScope) => {
// ...
var initExternal = (id) => {
var handleError = (err) => (warn("Initialization of sharing external failed: " + err));
try {
// 請(qǐng)求遠(yuǎn)程應(yīng)用
var module = __webpack_require__(id);
if(!module) return;
// 調(diào)用遠(yuǎn)程應(yīng)用的init方法,將當(dāng)前應(yīng)用的sharedScope賦值給
// 遠(yuǎn)程應(yīng)用的sharedScope
var initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))
// ...
} catch(err) { handleError(err); }
}
var promises = [];
switch(name) {
case "default": {
register("react-dom", "16.14.0", () => (Promise.all([__webpack_require__.e("vendors-node_modules_react-dom_index_js"),
__webpack_require__.e("webpack_sharing_consume_default_react_react")]).then(() => (() => (__webpack_require__(/*! ./node_modules/react-dom/index.js */
"./node_modules/react-dom/index.js"))))));
register("react", "16.14.0", () => (Promise.all([__webpack_require__.e("vendors-node_modules_react_index_js"),
__webpack_require__.e("node_modules_object-assign_index_js-node_modules_prop-types_checkPropTypes_js")]).then(() => (() => (__webpack_require__(/*!
./node_modules/react/index.js */ "./node_modules/react/index.js"))))));
initExternal("webpack/container/reference/component-app");
}
break;
}
};
})();
這里的 __webpack_require__.S 就是保存共享依賴的信息,它是應(yīng)用間共享依賴的橋梁。在經(jīng)過(guò) register 方法后,可以看到 webpack_require.S 保存的信息:

從上面我們看到 sharedScope 中保存了 react 和 react-dom 兩個(gè)共享依賴,每個(gè)共享依賴都有其對(duì)應(yīng)的版本號(hào)、來(lái)源以及獲取依賴的方法(get)。
接著就會(huì)調(diào)用 initExternal 方法去加載遠(yuǎn)程應(yīng)用 webpack/container/reference/component-ap,即 remoteEntry.js 文件,加載完之后就會(huì)調(diào)用他的 init 方法,下面我們看看 component 的 remoteEntry.js 中的 init 方法:
// shareScope表示Host應(yīng)用中的共享作用域
var init = (shareScope, initScope) => {
if (!__webpack_require__.S) return;
var name = "default"
var oldScope = __webpack_require__.S[name];
if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
// 將Host的sharedScope賦值給當(dāng)前應(yīng)用
__webpack_require__.S[name] = shareScope;
// 又調(diào)用當(dāng)前應(yīng)用的__webpack_require__.I方法去處理它的remote應(yīng)用
return __webpack_require__.I(name, initScope);
};
我們看到,init 方法會(huì)使用 main 應(yīng)用的 webpack_require.S 初始化 component 應(yīng)用的 webpack_require.S,由于是引用數(shù)據(jù)類型,所以 main 和 component 共用了一個(gè)的 sharedScope。
之后 main 應(yīng)用也調(diào)用了自己的 webpack_require.I,也會(huì) register 自己的共享依賴,最終的 webpack_require.S 如下:

因?yàn)?main 和 component 使用的是不同版本的依賴,所以最終的 sharedScope 中也會(huì)保存不同版本的依賴。
現(xiàn)在我們的共享作用域已經(jīng)初始化好了,接下來(lái)就是每個(gè)應(yīng)用根據(jù)自己的配置規(guī)則去共享作用域中獲取符合規(guī)則的依賴。
總結(jié)下流程:
當(dāng)應(yīng)用配置了 shared 后,那么依賴了這些共享依賴的模塊在加載前都會(huì)先調(diào)用 __webpack_require__.I 去初始化共享依賴,使用 __webpack_require__.S 對(duì)象來(lái)保存著每個(gè)應(yīng)用的共享依賴版本信息,每個(gè)應(yīng)用引用共享依賴時(shí),會(huì)根據(jù)不同的自己配制的規(guī)則從__webpack_require__.S 獲取到適合的依賴版本,__webpack_require__.S 是應(yīng)用間共享依賴的橋梁。
應(yīng)用場(chǎng)景
1、代碼共享
在 MF 中如果想暴露一些屬性、方法或者組件,只需要在 ModuleFederationPlugin 中配置一下 exposes,host 使用的時(shí)候則需要配置一下 remotes 就可以引用遠(yuǎn)程應(yīng)用暴露的值。
同時(shí)在使用的時(shí)候即可以通過(guò) 同步 的方式引用也可以通過(guò) 異步 的方式,比如在 main 應(yīng)用中想引入 component 應(yīng)用的 Button 組件:
同步引用
import Button from 'component-app/Button';
頁(yè)面的 chunk 會(huì)等待 component 應(yīng)用的 remoteEntry.js 下載完成再執(zhí)行。
異步引用
const Button = React.lazy(() => import('component-app/Button'));
頁(yè)面的 chunk 下載完成之后會(huì)立即執(zhí)行,然后再異步下載 component 應(yīng)用的 remoteEntry.js。
雖然 MF 能夠幫我們很好的解決代碼共享的問(wèn)題,但是新的開發(fā)模式也帶來(lái)了幾個(gè)問(wèn)題。
缺乏類型提示
在引用 remote 應(yīng)用的時(shí)候缺乏了類型提示,即使 remote 應(yīng)用有類型文件,但是 Host 應(yīng)用在引用的時(shí)候只是建立了一個(gè)引用關(guān)系,所以根本就獲取不到類型文件。
缺乏支持多個(gè)應(yīng)用同時(shí)啟動(dòng)同時(shí)開發(fā)的工具
隨著這種開發(fā)模式的普遍之后,一個(gè)頁(yè)面涉及到多個(gè)應(yīng)用的代碼是必然存在的,此時(shí)就需要有相應(yīng)的開發(fā)工具來(lái)支持。
2、公共依賴
由上面的例子我們知道,在 MF 中所有的公共依賴最終都會(huì)存放在一個(gè)公共作用域中,所有的應(yīng)用根據(jù)自己的配置規(guī)則找到相應(yīng)的依賴,這只需要我們?cè)?ModuleFederationPlugin 中配置好 shared 字段就行了:
new ModuleFederationPlugin({
name: 'main_app',
remotes: {
'component-app': 'component_app@http://localhost:3001/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
但是不僅僅是應(yīng)用依賴公共依賴,公共依賴之間也會(huì)相互依賴,比如 React-Dom 依賴 React,Mobx 依賴 React 和 React-Dom,最終的結(jié)構(gòu)如下所示:

這樣的話也會(huì)帶了一個(gè)性能問(wèn)題,因?yàn)槊總€(gè)應(yīng)用可能依賴的是不同依賴或者是相同依賴的不同版本,這樣的話項(xiàng)目在啟動(dòng)的時(shí)候需要異步下載非常多的資源,這個(gè)問(wèn)題其實(shí)和 vite 遇到的問(wèn)題是相似的,在 vite 中每一個(gè) import 其實(shí)就是一個(gè)請(qǐng)求,他們采用的方法是在預(yù)構(gòu)建的時(shí)候?qū)⒎稚⒌牡谌綆?kù)打包在一起從而減少請(qǐng)求的數(shù)量。
在 MF 中我們可以新建一個(gè)庫(kù)應(yīng)用用于存放所有的公共依賴,這樣也存在一個(gè)缺陷,就是解決不了多版本的問(wèn)題,因?yàn)樵趲?kù)應(yīng)用里裝不了兩個(gè)版本的依賴,如果不需要解決多版本的問(wèn)題,這種方式比較好一點(diǎn)。

總結(jié)
上面我們講了 MF 的基本概念到實(shí)現(xiàn)原理再到應(yīng)用場(chǎng)景,也介紹了在不同場(chǎng)景中存在的一些問(wèn)題,下面總結(jié)下他的優(yōu)缺點(diǎn):
優(yōu)點(diǎn):
能夠像微前端那樣將一個(gè)應(yīng)用拆分成多個(gè)相互獨(dú)立的子應(yīng)用,同時(shí)子應(yīng)用可以與技術(shù)棧無(wú)關(guān)。
能解解決應(yīng)用之間代碼共享的問(wèn)題,每個(gè)應(yīng)用都可以作為 host 和 remote。
提供了一套依賴共享機(jī)制,支持多版本。
缺點(diǎn):
為了實(shí)現(xiàn)依賴共享需要異步加載各種資源,容易造成頁(yè)面卡頓。
在引用遠(yuǎn)程應(yīng)用的組件 / 方法時(shí)沒(méi)有類型提示。
沒(méi)有統(tǒng)一的開發(fā)工具支持多個(gè)應(yīng)用同時(shí)啟動(dòng)同時(shí)開發(fā)。
Node 社群
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
“分享、點(diǎn)贊、在看” 支持一下
