欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JavaScript面試Module?Federation實(shí)現(xiàn)原理詳解

 更新時(shí)間:2022年10月08日 08:49:46   作者:西陵  
這篇文章主要為大家介紹了JavaScript面試Module?Federation實(shí)現(xiàn)原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jì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)建(1)

    這篇文章主要介紹了JavaScript?數(shù)據(jù)結(jié)構(gòu)之集合創(chuàng)建,集合是由一組無序且唯一的元素組成。數(shù)據(jù)結(jié)構(gòu)中的集合,對(duì)應(yīng)的是數(shù)學(xué)概念當(dāng)中的有限集合;下文詳細(xì)介紹需要的小伙伴可以參考一下
    2022-04-04
  • WebGL 顏色與紋理使用介紹

    WebGL 顏色與紋理使用介紹

    這篇文章主要為大家介紹了WebGL 顏色與紋理使用介紹,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-04-04
  • TypeScript中函數(shù)重載寫法

    TypeScript中函數(shù)重載寫法

    這篇文章主要介紹了TypeScript中函數(shù)重載寫法,TypeScript 提供了函數(shù)重載功能,下面文章圍繞TypeScript函數(shù)重載續(xù)航管資料展開內(nèi)容,具有一定得參考價(jià)值,需要的朋友可以參考一下
    2021-12-12
  • js浮點(diǎn)數(shù)保留兩位小數(shù)點(diǎn)示例代碼(四舍五入)

    js浮點(diǎn)數(shù)保留兩位小數(shù)點(diǎn)示例代碼(四舍五入)

    本篇文章主要介紹了js浮點(diǎn)數(shù)保留兩位小數(shù)點(diǎn)示例代碼(四舍五入) 需要的朋友可以過來參考下,希望對(duì)大家有所幫助
    2013-12-12
  • 淺談js常用內(nèi)置方法和對(duì)象

    淺談js常用內(nèi)置方法和對(duì)象

    下面小編就為大家?guī)硪黄獪\談js常用內(nèi)置方法和對(duì)象。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2016-09-09
  • javascript 循環(huán)調(diào)用示例介紹

    javascript 循環(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)證的示例代碼

    這篇文章主要介紹了原生JavaScript實(shí)現(xiàn)滑動(dòng)拖動(dòng)驗(yàn)證的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-12-12
  • 可多次使用的仿126郵箱選項(xiàng)卡的源碼

    可多次使用的仿126郵箱選項(xiàng)卡的源碼

    可多次使用的仿126郵箱選項(xiàng)卡的源碼...
    2007-10-10
  • 一個(gè)js過濾空格的小函數(shù)

    一個(gè)js過濾空格的小函數(shù)

    過濾空格,尤其是在一些注冊(cè)頁面比較實(shí)用,可以用js在客戶端將空格過濾掉,減輕服務(wù)器端的負(fù)擔(dān),下面是實(shí)現(xiàn)函數(shù)
    2014-10-10
  • js中同步與異步處理的方法和區(qū)別總結(jié)

    js中同步與異步處理的方法和區(qū)別總結(jié)

    若要在使用ajax請(qǐng)求后處理發(fā)送請(qǐng)求返回的結(jié)果,最好使用同步請(qǐng)求,下面為大家介紹下js中同步與異步處理的方法和區(qū)別,感興趣的朋友不要錯(cuò)過
    2013-12-12

最新評(píng)論