JavaScript面试Module Federation实现原理详解

目录
  • 基本概念
    • 1、什么是 Module Federation?
    • 2、Module Federation核心概念
    • 3、使用案例
    • 4、插件配置
  • 工作原理
    • 1、使用MF后在构建上有什么不同?
    • 2、如何加载远程模块?
    • 3、如何共享依赖?
  • 应用场景
    • 1、代码共享
    • 2、公共依赖
  • 总结

基本概念

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.

简单理解就是说 “一个应用可以由多个独立的构建组成,这些构建彼此独立没有依赖关系,他们可以独立开发、部署。这就是常被认为的微前端,但不局限于此”

MF 解决的问题其实和微前端有些类似,都是将一个应用拆分成多个子应用,每个应用都可以独立开发、部署,但是他们也有一些区别,比如微前端需要一个中心应用(简称基座)去承载子应用,而 MF 不需要,因为任何一个应用都可以作为中心应用,其次就是 MF 可以实现应用之间的依赖共享。

2、Module Federation核心概念

  • Container

一个使用 ModuleFederationPlugin 构建的应用就是一个 Container,它可以加载其他的 Container,也可以被其他的 Container 加载。

  • Host&Remote

从消费者和生产者的角度看 Container,Container 可以分为 Host 和 Remote,Host 作为消费者,他可以动态加载并运行其他 Remote 的代码,Remote 作为提供方,他可以暴露出一些属性、方法或组件供 Host 使用,这里要注意的一点是一个 Container 既可以作为 Host 也可以作为 Remote。

  • Shared

shared 表示共享依赖,一个应用可以将自己的依赖共享出去,比如 react、react-dom、mobx等,其他的应用可以直接使用共享作用域中的依赖从而减少应用的体积。

3、使用案例

下面通过一个实例来演示一下 MF 的功能,该项目由 main 和 component 两个应用组成,component 应用会将自己的组件 exposes 出去,并将 react 和 react-dom 共享出来给 main 应用使用。

完成代码可查看这里 github.com/projectcss/…

大家最好将源代码下载下来自己跑一遍便于理解,下面展示的是 main 应用的代码,在 App 组件中我们引入了 component 应用的 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;

效果如下:

我们看到,因为 main 应用 引用了 component 应用的组件,所以在渲染的时候需要异步去下载 component 应用的入口代码(remoteEntry)以及组件,同时只下载了 main 应用共享出去的 react 和 react-dom 这两个依赖,也就是说 component 中的组件使用的就是 main 应用 提供的依赖,这样就实现了代码动态加载以及依赖共享的功能。

4、插件配置

为了实现联邦模块的功能,webpack 接住了一个插件 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等组件暴露出去,同时将 react 和 react-dom 这两个依赖共享出去。

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 } },
    })
  ],
};

作为消费者的 main 应用需要定义需要消费的应用的名称以及地址,同时 main 应用也将自己的 react 和 react-dom 这两个依赖共享出去。

下面来介绍几个核心的配置字段:

  • name

name 表示当前应用的别名,当作为 remote 时被 host引用时需要在路径前加上这个前缀,比如 main 中的 remote 配置:

remotes: {
    'component-app': 'component_app@http://localhost:3001/remoteEntry.js',
},

路径的前缀 component_app 就是 component 应用的 name 值。

  • filename filename 表示 remote 应用提供给 host 应用使用时的入口文件,比如上面 component 应用设置的是 remoteEntry,那么在最终的构建产物中就会出现一个 remoteEntry.js 的入口文件供 main 应用加载。
  • exposes

exposes 表示 remote 应用有哪些属性、方法和组件需要暴露给 host 应用使用,他是一个对象,其中 key 表示在被 host 使用的时候的相对路径,value 则是当前应用暴露出的属性的相对路径,比如在引入 Button 组件时可以这么写:

import Button from 'component-app/Button';
  • remote

remote 表示当前 host 应用需要消费的 remote 应用的以及他的地址,他是一个对象,key 为对应 remote 应用的 name 值,这里要注意这个 name 不是 remote 应用中配置的 name,而是自己为该 remote 应用自定义的值,value 则是 remote 应用的资源地址。

  • shared

当前应用无论是作为 host 还是 remote 都可以共享依赖,而共享的这些依赖需要通过 shared 去指定。

new ModuleFederationPlugin({
  name: 'main_app',
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})

他的配置方式有三种,具体可以查看官网,这里只介绍常用的对象配置形式,在对象中 key 表示第三方依赖的名称,value 则是配置项,常用的配置项有 singleton 和 requiredVersion。

  • singleton 表示是否开启单例模式,如果开启的话,共享的依赖则只会加载一次(优先取版本高的)。
  • requiredVersion 表示指定共享依赖的版本。

比如 singleton 为 true 时,main 的 react 版本为 16.14.0,component 的 react 版本为 16.13.0,那么 main 和 component 将会共同使用 16.14.0 的 react 版本,也就是 main 提供的 react。

如果这时 component 的配置中将 react 的 requiredVersion 设置为 16.13.0,那么 component 将会使用 16.13.0,main 将会使用 16.14.0,相当于它们都没有共享依赖,各自下载自己的 react 版本。

工作原理

1、使用MF后在构建上有什么不同?

在没有使用 MF 之前,component,lib 和 main 的构建如下:

使用 MF 之后构建结果如下:

对比上面两张图我们可以看出使用 MF 构建出的产物发生了变化,里面新增了 remoteEntry-chunk、shared-chunk、expose-chunk 以及 async-chunk。

其中 remoteEntry-chunk、shared-chunk 和 expose-chunk 是因为使用了 ModuleFederationPlugin 而生成的,async-chunk 是因为我们使用了异步导入 import() 而产生的。

下面我们对照着 component 的插件配置介绍一下每个 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 是当前应用作为远程应用(Remote)被调用的时候请求的文件,对应的文件名为插件里配置的 filename,我们当前设置的名称就叫做 remoteEntry.js,我们可以打开 main 应用的控制台查看:

  • shared-chunk 是当前应用开启了 shared 功能后生成的,比如我们在 shared 中指定了 react 和 react-dom,那么在构建的时候 react 和 react-dom 就会被分离成新的 shared-chunk,比如 vendors-node_modules_react_index_js.js 和 vendors-node_modules_react-dom_index_js.js。
  • espose-chunk 是当前应用暴露一些属性/组件给外部使用的时候生成的,在构建的时候会根据 exposes 配置项生成一个或多个 expose-chunk,比如 src_Button_jsx.js、src_Dialog_jsx.js 和 src_ToolTip_jsx.js。
  • async-chunk 是一个异步文件,这里指的其实就是 bootstrap_js.js,为什么需要生成一个异步文件呢?我们看看 main 应用中的 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')

一般在我们的项目中 index.js 作为我们的入口文件里面应该存放的是 bootstrap.js 中的代码,这里却将代码单独抽离出来放到 bootstrap.js 中,同时在 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 就会被立即执行了,但是此时 component 的资源根本没有被下载下来,所以就会报错。

如果我们开启了 shared 功能的话,那么 import React from 'react' 这句被同步执行也会报错,因为这时候还没有初始化好共享的依赖。

所以必须把原来的入口代码放到 bootstrap.js 里面,在 index.js 中使用 import 来异步加载 bootstrap.js ,这样可以实现先加载 main.js,然后在异步加载 bootstrap_js.js(async-chunk) 的时候先加载好远程应用的资源并初始化好共享的依赖,最后再执行 bootstrap.js 模块。

2、如何加载远程模块?

我们先看一下 webpack 是怎么转换 main 应用中的导入语句: 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 中找到编译后的结果:

我们可以看到 component-app/Button 最终会被编译成 webpack/container/remote/component-app/Button,但是 webpack/container/remote/component-app/Button 又在哪呢,我们从 main 应用的 入口文件 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 就是加载远程模块的核心代码,代码中有个 chunkMapping 对象,这个对象保存的是当前应用的那些模块依赖了远程应用,idToExternalAndNameMapping 对象保存的是被依赖的远程模块的基本信息,便于后面远程请求该模块。

在加载 bootstrap_js.js 的时候必须先加载完远程应用的资源,对于我们的例子来说如果我们想要使用远程应用中的 Button、Tooltip 组件就必须先加载这个应用的资源,即 webpack/container/reference/component-app,这个从 handleFunction 方法中就可以看出来,data[2] 也就代表着 idToExternalAndNameMapping 中每一项对应的数组的第二项数据,下面我们在 main.js 中找到 webpack/container/reference/component-app

这里会异步去加载 component 的 remoteEntry.js,也就是我们在 main 应用中配置 ModuleFederationPlugin 的时候制定的 component 远程模块的入口文件的资源地址,加载完后返回 componnet_app 这个全局变量作为 webpack/container/reference/component-app 模块的输出值,这里有两个点要注意:

  • 这里是通过 JSONP 的形式去加载远程应用,拿到远程应用的 remoteEntry.js 文件后再去执行。
  • componnet_app 是 入口文件 remoteEntry.js 中的一个全局变量,再执行该文件的时候会往这个全局变量上挂载属性,这个后面会介绍。

但是这里我们只是获得了 componnet_app 这个远程模块的输出值,但是怎么获取到 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 应用的入口文件 main.js ,在 __webpack_require__.f.remotes 里有一个方法:

var onInitialized = (_, external, first) => (handleFunction(external.get, data[1], getScope, 0, onFactory, first));

这里 external.get 其实就是 componnet_app.get 方法,data[1] 就是我们的要加载的组件,比如执行 componnet_app.get('./Button')就可以异步获取 Button 组件。

下面总结一下整个流程,main 应用首先会去执行入口文件 main.js,然后加载 bootstrap_js 模块,判断他依赖了远程模块 webpack/container/remote/component-app/Button,...,那么先会去下载远程模块 webpack/container/remote/component-app,即 remoteEntry.js ,然后返回 component_app 这个全局变量,然后执行 component-app.get('./xxx') 去获取对应的组件,等远程应用的资源以及 bootstrap_js资源全部下载完成后再执行 bootstrap.js模块。

3、如何共享依赖?

在 webpack 的构建中每个构建产物之间都是隔离的,而要实现依赖共享就需要打破这个隔离,这里的关键在于 sharedScope(共享作用域),我们需要在 Host 和 Remote 应用之间建立一个可共享的 sharedScope,里面包含了所有可共享的依赖,之后都按照一定的规则从这个共享作用域中获取相应的依赖。

为了探究 webpack 到底是怎么实现依赖共享的,我们首先看 main 应用的入口文件 main.js

// 共享模块与对应加载地址映射
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")))))))
};
// 当前应用依赖的共享模块
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 {
                // 调用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 功能后会多了上面这部分逻辑,其中 chunkMapping 这个对象保存的是当前应用有哪些模块依赖了共享依赖,比如 bootstrap_js 依赖了 react 和 react-dom 这两个共享依赖。

那么在加载 bootstrap_js 的时候就必须先加载完这些共享依赖,这些以来都是通过 loadSingletonVersionCheckFallback 这个方法进行加载的,下面我们来看看这个方法:

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);
});

在执行 loadSingletonVersionCheckFallback之前,首先要执行了 init方法,init 方法中又会调用 webpack_require.I ,现在就来到了共享依赖的重点:

(() => {
    __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 {
                // 请求远程应用
                var module = __webpack_require__(id);
                if(!module) return;
                // 调用远程应用的init方法,将当前应用的sharedScope赋值给
                // 远程应用的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 就是保存共享依赖的信息,它是应用间共享依赖的桥梁。在经过 register 方法后,可以看到 webpack_require.S 保存的信息:

从上面我们看到 sharedScope 中保存了 react 和 react-dom 两个共享依赖,每个共享依赖都有其对应的版本号、来源以及获取依赖的方法(get)。

接着就会调用 initExternal 方法去加载远程应用 webpack/container/reference/component-ap,即 remoteEntry.js 文件,加载完之后就会调用他的 init 方法,下面我们看看 component 的 remoteEntry.js 中的 init 方法:

// shareScope表示Host应用中的共享作用域
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赋值给当前应用
    __webpack_require__.S[name] = shareScope;
    // 又调用当前应用的__webpack_require__.I方法去处理它的remote应用
    return __webpack_require__.I(name, initScope);
};

我们看到,init 方法会使用 main 应用的 webpack_require.S 初始化 component 应用的webpack_require.S,由于是引用数据类型,所以 main 和 component 共用了一个的 sharedScope。

之后 main 应用也调用了自己的 webpack_require.I,也会 register 自己的共享依赖,最终的 webpack_require.S 如下:

因为 main 和 component 使用的是不同版本的依赖,所以最终的 sharedScope 中也会保存不同版本的依赖。

现在我们的共享作用域已经初始化好了,接下来就是每个应用根据自己的配置规则去共享作用域中获取符合规则的依赖。

总结下流程:

当应用配置了 shared 后,那么依赖了这些共享依赖的模块在加载前都会先调用 __webpack_require__.I 去初始化共享依赖,使用 __webpack_require__.S 对象来保存着每个应用的共享依赖版本信息,每个应用引用共享依赖时,会根据不同的自己配制的规则从__webpack_require__.S 获取到适合的依赖版本,__webpack_require__.S是应用间共享依赖的桥梁。

应用场景

1、代码共享

在 MF 中如果想暴露一些属性、方法或者组件,只需要在 ModuleFederationPlugin 中配置一下 exposes,host 使用的时候则需要配置一下 remotes 就可以引用远程应用暴露的值。

同时在使用的时候即可以通过 同步 的方式引用也可以通过 异步 的方式,比如在 main 应用中想引入 component 应用的 Button 组件:

同步引用:

import Button from 'component-app/Button';

页面的 chunk 会等待 component 应用的 remoteEntry.js 下载完成再执行。

异步引用

const Button = React.lazy(() => import('component-app/Button'));

页面的 chunk 下载完成之后会立即执行,然后再异步下载 component 应用的 remoteEntry.js。

虽然 MF 能够帮我们很好的解决代码共享的问题,但是新的开发模式也带来了几个问题。

  • 缺乏类型提示
  • 在引用 remote 应用的时候缺乏了类型提示,即使 remote 应用有类型文件,但是 Host 应用在引用的时候只是建立了一个引用关系,所以根本就获取不到类型文件。
  • 缺乏支持多个应用同时启动同时开发的工具
  • 随着这种开发模式的普遍之后,一个页面涉及到多个应用的代码是必然存在的,此时就需要有相应的开发工具来支持。

2、公共依赖

由上面的例子我们知道,在 MF 中所有的公共依赖最终都会存放在一个公共作用域中,所有的应用根据自己的配置规则找到相应的依赖,这只需要我们在 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 } },
}),

但是不仅仅是应用依赖公共依赖,公共依赖之间也会相互依赖,比如 React-Dom 依赖 React,Mobx 依赖 React 和 React-Dom,最终的结构如下所示:

这样的话也会带了一个性能问题,因为每个应用可能依赖的是不同依赖或者是相同依赖的不同版本,这样的话项目在启动的时候需要异步下载非常多的资源,这个问题其实和 vite 遇到的问题是相似的,在 vite 中每一个 import 其实就是一个请求,他们采用的方法是在预构建的时候将分散的第三方库打包在一起从而减少请求的数量。

在 MF 中我们可以新建一个库应用用于存放所有的公共依赖,这样也存在一个缺陷,就是解决不了多版本的问题,因为在库应用里装不了两个版本的依赖,如果不需要解决多版本的问题,这种方式比较好一点。

总结

上面我们讲了 MF 的基本概念到实现原理再到应用场景,也介绍了在不同场景中存在的一些问题,下面总结下他的优缺点:

优点:

  • 能够像微前端那样将一个应用拆分成多个相互独立的子应用,同时子应用可以与技术栈无关。
  • 能解解决应用之间代码共享的问题,每个应用都可以作为 host 和 remote。
  • 提供了一套依赖共享机制,支持多版本。

缺点:

  • 为了实现依赖共享需要异步加载各种资源,容易造成页面卡顿。
  • 在引用远程应用的组件/方法时没有类型提示。
  • 没有统一的开发工具支持多个应用同时启动同时开发。

以上就是JavaScript面试Module Federation实现原理详解的详细内容,更多关于JS面试Module Federation原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • JS面试异步模拟红绿灯实现详解

    目录 引言 问题拆解 代码设计 引言 异步的问题是程序员绕不开的话题,无论在实际开发上还是在面试中,我们总会遇到各种头疼的问题,但是细细品味,其实这些问题总能拓展我们的思路,激发我们的思维能力.培养我们的高情商能力. (我,我编不下去了)红绿灯模拟在异步问题上是经典中的经典,网络上的设计也是层出不穷,我将给大家呈现一款比较独特的设计. 问题拆解 红绿灯在我们日常生活是常见的,因此对于问题大家是容易理解的,我们此次需要考虑如何模拟红绿灯的无间断切换和轮询过程. 首先分析,我们可以通过while循环

  • js面试题之异步问题的深入理解

    js中的宏任务与微任务 在面试过程中,基本面试官都会问你一些promise的问题,promise是es6的新内容,主要是用来优化异步的问题.笔试中经常会让你写一些promise和setTimeout的执行结果,这你就必须知道宏任务和微任务的概念了! 为什么要使用promise 如果你经历过以前的jquery开发项目,你会遇到以下问题:回调地狱 $.ajax({ ... success: function() { ... $.ajax({ ... success: function() { } }

  • JS前端面试手写apply和bind实例

    目录 前言 apply && bind apply && bind 作用 相同点 VS 不同点 轻松手写 手写实现 apply 手写实现 bind 总结 前言 面试官问:“聊一聊你理解的 apply 和 bind.” 于是我便开始开始介绍起这两个知识点,最后顺带提了它们的实现代码. 这不提倒还好,一提就出了大事,一下子给面试官找到了面试题目. 面试官紧接着说:“既然你提到了代码,那就手写一下它俩吧.” 我一下子不知所措.虽然我了解过 apply 和 bind 手写代码,但是

  • js前端面试之同步与异步问题详解

    前言 我本来是打算写一篇co源码精读(为啥读co,因为它短),然鹅发现自己存在一系列基础问题没有搞透彻,打算写一个js基础系列文章,总结自己的理解(copy),希望与你在学习路上一同进步.首先问问自己当面试官问到js中的同步和异步,这个问题该怎么回答?理解一个问题无非是what-why-how js同步和异步问题是什么-->为什么会产生异步问题-->如何解决. 一.JavaScript起源 技术的出现,和应用场景密切相关的.JavaScript诞生于1995年.当时,它的主要目的是处理以前由服

  • JavaScript面试数组index和对象key问题详解

    目录 面试题一: 1.数组赋值 2.数组取值 面试题二: 1.对象赋值 2.对象取值 总结 面试题一: var arr = [1, 2, 3, 4] 复制代码 问:arr[1] = ?; arr['1'] = ? 答:arr[1] = 2; arr['1'] = 2 这里可以再分为两个问题: 1.数组赋值 var arr = [1, 2, 3, 4] arr[1] = 10; // 数字场景 arr['10'] = 1; // 字符串场景 arr['a'] = 1; // 字符串场景 arr[t

  • JS面试之异步模拟超时重传机制详解

    目录 引言 题目分析 代码设计 核心讲解 引言 前面我讲解了两篇有关异步的逻辑思维题目,一个是红绿灯转换,还有一个是异步并发限制.有小伙伴私信我说不过瘾,希望还能再出一篇异步超时重传的讲解.为了满足这位粉丝的小小要求(我尼玛),我查询了相关资料和面试题,确实发现这是某大肠面试的代码设计题.不得不说这位粉丝发现的这个题目是相当亮眼,相当给力. 题目分析 超时重传机制,看到这个词语想必科班同学都十分十分熟悉吧.大家第一时间肯定会想到计算机网络中tcp的超时重传.不错,此处的异步模拟超时重传机制和计算

  • JS面试之对事件循环的理解

    目录 一.是什么 事件循环(Event Loop) 二.宏任务与微任务 微任务 宏任务 三.async与await async await 四.流程分析 一.是什么 JavaScript 在设计之初便是单线程,即指程序运行时,只有一个线程存在,同一时间只能做一件事 为什么要这么设计,跟JavaScript的应用场景有关 JavaScript 初期作为一门浏览器脚本语言,通常用于操作 DOM ,如果是多线程,一个线程进行了删除 DOM ,另一个添加 DOM,此时浏览器该如何处理? 为了解决单线程运

  • js面试题继承的方法及优缺点解答

    目录 说一说js继承的方法和优缺点? 一.原型链继承 二.借用构造函数(经典继承) 三.组合继承 四.原型式继承 五.寄生式继承 六.寄生组合式继承 说一说js继承的方法和优缺点? 要点: 原型链继承.借用构造函数继承.组合继承.原型式继承.寄生式继承.寄生组合式继承.ES6 Class 答: 一.原型链继承 缺点: 1.引用类型的属性被所有实例共享 2.在创建 Child 的实例时,不能向 Parent 传参 //原型链继承 function Parent() { this.parentPro

  • JavaScript面试Module Federation实现原理详解

    目录 基本概念 1.什么是 Module Federation? 2.Module Federation核心概念 3.使用案例 4.插件配置 工作原理 1.使用MF后在构建上有什么不同? 2.如何加载远程模块? 3.如何共享依赖? 应用场景 1.代码共享 2.公共依赖 总结 基本概念 1.什么是 Module Federation? 首先看一下官方给出的解释: Multiple separate builds should form a single application. These sep

  • Javascript对象及Proxy工作原理详解

    正文 这一章其实算是javascript的科普文章,其实这本书的读者一般都不会是入门者,因此按道理说应该不需要再科普才对.但是作者依旧安排了这一章,证明就是这一章内容与我们以为的对象不一样. Javascript中一切皆对象 这一句话大家应该耳熟能详,对于常规的字面量对象,和new出来的对象,大家应该都能分辨 const str = '' const str2 = new String() const obj = {} const obj2 = Object.create() 但是根据ECMA,

  • JavaScript装饰器的实现原理详解

    目录 装饰器的常见作用 装饰类的属性 装饰类 注意 实例应用 最近在使用TS+Vue的开发模式,发现项目中大量使用了装饰器,看得我手足无措,今天特意研究一下实现原理,方便自己理解这块知识点. 装饰器的常见作用 装饰一个类的属性 装饰一个类 装饰器只能针对类和类的属性,不能直接作用于函数,因为存在函数提升. 下面我们针对这两种情况进行举例阐述. 装饰类的属性 function readonly(target, name, descriptor) { discriptor.writable = fa

  • JavaScript开发简单易懂的Svelte实现原理详解

    目录 Demo1 create_fragment SvelteComponent 可以改变状态的Demo Svelte问世很久了,一直想写一篇好懂的原理分析文章,拖了这么久终于写了. Demo1 首先来看编译时,考虑如下App组件代码: <h1>{count}</h1> <script> let count = 0; </script> 这段代码经由编译器编译后产生如下代码,包括三部分: create_fragment方法 count的声明语句 class

  • JavaScript ES6 Class类实现原理详解

    JavaScript ES6之前的还没有Class类的概念,生成实例对象的传统方法是通过构造函数. 例如: function Mold(a,b){ this.a=a; this.b=b; } Mold.prototype.count=function(){ return this.a+this.b; }; let sum=new Mold(1,2); console.log(sum.count()) //3 这中写法跟传统的面向对象语言差异较大,写起来也比较繁杂. ES6提供了更加接近其他语言的

  • Javascript原型链及instanceof原理详解

    instanceof:用来判断实例是否是属于某个对象,这个判断依据是什么呢? 首先,了解一下javascript中的原型继承的基础知识: javascript中的对象都有一个__proto__属性,这个是对象的隐式原型,指向该对象的父对象的原型(prototype).显式的原型对象使用prototype,但是Object.prototype.proto=null; 判断某个对象a是否属于某个类A的实例,可以通过搜索原型链. 实例对象属性查找顺序是:实例对象内部---->构造函数原型链---->

  • JavaScript原型继承和原型链原理详解

    这篇文章主要介绍了JavaScript原型继承和原型链原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在讨论原型继承之前,先回顾一下关于创建自定义类型的方式,这里推荐将构造函数和原型模式组合使用,通过构造函数来定义实例自己的属性,再通过原型来定义公共的方法和属性. 这样一来,每个实例都有自己的实例属性副本,又能共享同一个方法,这样的好处就是可以极大的节省内存空间.同时还可以向构造函数传递参数,十分的方便. 这里还要再讲一下两种特色的构造

  • JavaScript对象原型链原理详解

    这篇文章主要介绍了JavaScript对象原型链原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一个js对象,除了自己设置的属性外,还会自动生成proto.class.extensible属性,其中,proto属性指向对象的原型. 对象的属性也有writable.enumerable.configurable.value和get/set的配置方法. 对象的创建方式有三种: 一.使用字面量直接创建. 二.基于原型链创建. 分析上图,要点如

  • javascript异常处理实现原理详解

    这篇文章主要介绍了javascript异常处理实现原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.什么是例外处理 当 JavaScript程序在运行中发生了诸如数组索引越界.类型不匹配或者语法错误时,JavaScript解释器就会引发例外处理. ECMAScript定义了六种类型的错误,除此之外,我们可以使用Error对象和throw语句来创建并引发自定义的例外处理信息. 通过运用例外处理技术,我们可以实现用结构化的方式来响应错误事

  • Vue面试中observable原理详解

    目录 一.Observable 是什么 二.使用场景 三.原理分析 一.Observable 是什么 Observable 翻译过来我们可以理解成可观察的 我们先来看一下其在Vue中的定义 Vue.observable,让一个对象变成响应式数据.Vue 内部会用它来处理 data 函数返回的对象 返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新.也可以作为最小化的跨组件状态存储器 Vue.observable({ count : 1}) 其作用等同于 new vue(

随机推荐