JavaScript面試Module?Federation實(shí)現(xiàn)原理詳解
基本概念
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.
簡單理解就是說 “一個(gè)應(yīng)用可以由多個(gè)獨(dú)立的構(gòu)建組成,這些構(gòu)建彼此獨(dú)立沒有依賴關(guān)系,他們可以獨(dú)立開發(fā)、部署。這就是常被認(rèn)為的微前端,但不局限于此”
MF 解決的問題其實(shí)和微前端有些類似,都是將一個(gè)應(yīng)用拆分成多個(gè)子應(yīng)用,每個(gè)應(yīng)用都可以獨(dú)立開發(fā)、部署,但是他們也有一些區(qū)別,比如微前端需要一個(gè)中心應(yīng)用(簡稱基座)去承載子應(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、使用案例
下面通過一個(gè)實(shí)例來演示一下 MF 的功能,該項(xiàng)目由 main 和 component 兩個(gè)應(yīng)用組成,component 應(yīng)用會(huì)將自己的組件 exposes 出去,并將 react 和 react-dom 共享出來給 main 應(yīng)用使用。
完成代碼可查看這里 github.com/projectcss/…
大家最好將源代碼下載下來自己跑一遍便于理解,下面展示的是 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è)依賴,也就是說 component 中的組件使用的就是 main 應(yīng)用 提供的依賴,這樣就實(shí)現(xiàn)了代碼動(dòng)態(tài)加載以及依賴共享的功能。
4、插件配置
為了實(shí)現(xiàn)聯(lián)邦模塊的功能,webpack 接住了一個(gè)插件 ModuleFederationPlugin,下面我們就拿上面的例子來介紹插件的配置。
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è)依賴共享出去。
下面來介紹幾個(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)用無論是作為 host 還是 remote 都可以共享依賴,而共享的這些依賴需要通過 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)于它們都沒有共享依賴,各自下載自己的 react 版本。
工作原理
1、使用MF后在構(gòu)建上有什么不同?
在沒有使用 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ú)抽離出來放到 bootstrap.js 中,同時(shí)在 index.js 中使用 import('./bootstrap') 來異步加載 bootstrap.js,這是為什么呢?
我們來看下這段代碼:
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 的資源根本沒有被下載下來,所以就會(huì)報(bào)錯(cuò)。
如果我們開啟了 shared 功能的話,那么 import React from 'react' 這句被同步執(zhí)行也會(huì)報(bào)錯(cuò),因?yàn)檫@時(shí)候還沒有初始化好共享的依賴。
所以必須把原來的入口代碼放到 bootstrap.js 里面,在 index.js 中使用 import 來異步加載 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)入語句: 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ì)于我們的例子來說如果我們想要使用遠(yuǎn)程應(yīng)用中的 Button、Tooltip 組件就必須先加載這個(gè)應(yīng)用的資源,即 webpack/container/reference/component-app,這個(gè)從 handleFunction 方法中就可以看出來,data[2] 也就代表著 idToExternalAndNameMapping 中每一項(xiàng)對(duì)應(yīng)的數(shù)組的第二項(xiàng)數(shù)據(jù),下面我們?cè)?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)要注意:
- 這里是通過 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 組件呢?
我們先來看一下 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í)候就必須先加載完這些共享依賴,這些以來都是通過 loadSingletonVersionCheckFallback 這個(gè)方法進(jìn)行加載的,下面我們來看看這個(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)在就來到了共享依賴的重點(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)過 register 方法后,可以看到 webpack_require.S 保存的信息:
從上面我們看到 sharedScope 中保存了 react 和 react-dom 兩個(gè)共享依賴,每個(gè)共享依賴都有其對(duì)應(yīng)的版本號(hào)、來源以及獲取依賴的方法(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)初始化好了,接下來就是每個(gè)應(yīng)用根據(jù)自己的配置規(guī)則去共享作用域中獲取符合規(guī)則的依賴。
總結(jié)下流程:
當(dāng)應(yīng)用配置了 shared 后,那么依賴了這些共享依賴的模塊在加載前都會(huì)先調(diào)用 __webpack_require__.I 去初始化共享依賴,使用 __webpack_require__.S 對(duì)象來保存著每個(gè)應(yīng)用的共享依賴版本信息,每個(gè)應(yīng)用引用共享依賴時(shí),會(huì)根據(jù)不同的自己配制的規(guī)則從__webpack_require__.S 獲取到適合的依賴版本,__webpack_require__.S是應(yīng)用間共享依賴的橋梁。
應(yīng)用場景
1、代碼共享
在 MF 中如果想暴露一些屬性、方法或者組件,只需要在 ModuleFederationPlugin 中配置一下 exposes,host 使用的時(shí)候則需要配置一下 remotes 就可以引用遠(yuǎn)程應(yīng)用暴露的值。
同時(shí)在使用的時(shí)候即可以通過 同步 的方式引用也可以通過 異步 的方式,比如在 main 應(yīng)用中想引入 component 應(yīng)用的 Button 組件:
同步引用:
import Button from 'component-app/Button';
頁面的 chunk 會(huì)等待 component 應(yīng)用的 remoteEntry.js 下載完成再執(zhí)行。
異步引用
const Button = React.lazy(() => import('component-app/Button'));
頁面的 chunk 下載完成之后會(huì)立即執(zhí)行,然后再異步下載 component 應(yīng)用的 remoteEntry.js。
雖然 MF 能夠幫我們很好的解決代碼共享的問題,但是新的開發(fā)模式也帶來了幾個(gè)問題。
- 缺乏類型提示
- 在引用 remote 應(yīng)用的時(shí)候缺乏了類型提示,即使 remote 應(yīng)用有類型文件,但是 Host 應(yīng)用在引用的時(shí)候只是建立了一個(gè)引用關(guān)系,所以根本就獲取不到類型文件。
- 缺乏支持多個(gè)應(yīng)用同時(shí)啟動(dòng)同時(shí)開發(fā)的工具
- 隨著這種開發(fā)模式的普遍之后,一個(gè)頁面涉及到多個(gè)應(yīng)用的代碼是必然存在的,此時(shí)就需要有相應(yīng)的開發(fā)工具來支持。
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è)性能問題,因?yàn)槊總€(gè)應(yīng)用可能依賴的是不同依賴或者是相同依賴的不同版本,這樣的話項(xiàng)目在啟動(dòng)的時(shí)候需要異步下載非常多的資源,這個(gè)問題其實(shí)和 vite 遇到的問題是相似的,在 vite 中每一個(gè) import 其實(shí)就是一個(gè)請(qǐng)求,他們采用的方法是在預(yù)構(gòu)建的時(shí)候?qū)⒎稚⒌牡谌綆齑虬谝黄饛亩鴾p少請(qǐng)求的數(shù)量。
在 MF 中我們可以新建一個(gè)庫應(yīng)用用于存放所有的公共依賴,這樣也存在一個(gè)缺陷,就是解決不了多版本的問題,因?yàn)樵?strong>庫應(yīng)用里裝不了兩個(gè)版本的依賴,如果不需要解決多版本的問題,這種方式比較好一點(diǎn)。
總結(jié)
上面我們講了 MF 的基本概念到實(shí)現(xiàn)原理再到應(yīng)用場景,也介紹了在不同場景中存在的一些問題,下面總結(jié)下他的優(yōu)缺點(diǎn):
優(yōu)點(diǎn):
- 能夠像微前端那樣將一個(gè)應(yīng)用拆分成多個(gè)相互獨(dú)立的子應(yīng)用,同時(shí)子應(yīng)用可以與技術(shù)棧無關(guān)。
- 能解解決應(yīng)用之間代碼共享的問題,每個(gè)應(yīng)用都可以作為 host 和 remote。
- 提供了一套依賴共享機(jī)制,支持多版本。
缺點(diǎn):
- 為了實(shí)現(xiàn)依賴共享需要異步加載各種資源,容易造成頁面卡頓。
- 在引用遠(yuǎn)程應(yīng)用的組件/方法時(shí)沒有類型提示。
- 沒有統(tǒng)一的開發(fā)工具支持多個(gè)應(yīng)用同時(shí)啟動(dòng)同時(shí)開發(fā)。
以上就是JavaScript面試Module Federation實(shí)現(xiàn)原理詳解的詳細(xì)內(nèi)容,更多關(guān)于JS面試Module Federation原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript?數(shù)據(jù)結(jié)構(gòu)之集合創(chuàng)建(1)
這篇文章主要介紹了JavaScript?數(shù)據(jù)結(jié)構(gòu)之集合創(chuàng)建,集合是由一組無序且唯一的元素組成。數(shù)據(jù)結(jié)構(gòu)中的集合,對(duì)應(yīng)的是數(shù)學(xué)概念當(dāng)中的有限集合;下文詳細(xì)介紹需要的小伙伴可以參考一下2022-04-04js浮點(diǎn)數(shù)保留兩位小數(shù)點(diǎn)示例代碼(四舍五入)
本篇文章主要介紹了js浮點(diǎn)數(shù)保留兩位小數(shù)點(diǎn)示例代碼(四舍五入) 需要的朋友可以過來參考下,希望對(duì)大家有所幫助2013-12-12javascript 循環(huán)調(diào)用示例介紹
循環(huán)調(diào)用,如果已經(jīng)獲取到了結(jié)果,則退出循環(huán),下面有個(gè)不錯(cuò)的示例,感興趣的朋友可以嘗試操作下2013-11-11原生JavaScript實(shí)現(xiàn)滑動(dòng)拖動(dòng)驗(yàn)證的示例代碼
這篇文章主要介紹了原生JavaScript實(shí)現(xiàn)滑動(dòng)拖動(dòng)驗(yàn)證的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12