详解 Module Federation 的实现原理

code秘密花园 发表于 1年以前  | 总阅读数:646 次

基本概念

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 应用使用。

完成代码可查看这里 https://github.com/projectcss/react-mf

大家最好将源代码下载下来自己跑一遍便于理解,下面展示的是 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.jsvendors-node_modules_react-dom_index_js.js
  • espose-chunk 是当前应用暴露一些属性 / 组件给外部使用的时候生成的,在构建的时候会根据 exposes 配置项生成一个或多个 expose-chunk,比如 src_Button_jsx.jssrc_Dialog_jsx.jssrc_ToolTip_jsx.js
  • async-chunk 是一个异步文件,这里指的其实就是 bootstrap_js.js,为什么需要生成一个异步文件呢?我们看看 main 应用中的 bootstrap.jsindex.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。
  • 提供了一套依赖共享机制,支持多版本。

缺点:

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

作者:@西陵
原文:https://juejin.cn/post/7151281452716392462

本文由微信公众号code秘密花园原创,哈喽比特收录。
文章来源:https://mp.weixin.qq.com/s/xxSESgw-MfhNJXRTZhhXlg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录