漫谈构建工具(十五): 初探 webpack5 - module federation

7,214 阅读20分钟

前言

最近,项目组在针对原来的老项目做技术栈升级工作,升级的主要内容是 react 和 antd。经过讨论,最后采取的技术方案是使用 qiankun。具体实施方案为先基于 qiankun 构建一个主应用,然后再部署一个新的子应用,新的子应用使用最新版本的 react、antd 逐步重构老的业务,最后将新旧两个应用接入到主应用里面。

qiankun 是一个基于 single-spa 的微前端实现库。使用 qiankun,我们可以无需关注子应用之间所使用的技术栈,子应用之间还可以做到独立开发、独立部署、独立运行、增量升级,非常方便。但是在使用过程中,发现有一个小问题。那就是如果子应用之间使用的技术栈一样且版本一致时,会导致依赖部分如(react、react-dom、antd 等) 重复加载,对性能造成影响。

那有没有解决的方法呢?

答案是有的。 新发布的 webpack5 提供的 module federation 新特性,可以帮助我们解决上述问题,并且提供了一种新的微前端实现方案。

针对 module federation, 本文将从以下几个维度进行介绍:

什么是 module federation

关于 module federation,webpack5 官方文档给出了相关说明,原文如下:

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.

解释如下:一个应用可以由多个独立的构建组成。这些独立的构建之间没有依赖关系,他们可以独立开发、部署。

上述解释,其实就是微前端的概念。

使用 module federation,我们可以在一个 javascript 应用中动态加载并运行另一个 javascript 应用的代码,并实现应用之间的依赖共享。

module federation 的用法

了解完 module federation 的概念以及用途以后,我们再来了解一下 module federation 该怎么使用。

webpack5 官方文档的结尾,提供了一个示例链接:module-federation-examples, 里面包含了大量 demo,详细介绍了 module federation 的各类应用场景。

本文将会基于示例: /advanced-api/automatic-vendor-sharing 来介绍 module federation 的用法。(感兴趣的同学可以凭借上述两个链接,查看具体的 demo)。

示例 automatic-vendor-sharing 非常简单,内部提供了两个基于 react 实现的应用 app1、app2。通过 module federation,app1、app2 可以互相使用对方的 Button 组件,并实现了 react、react-dom 依赖的的共享。

app1 和 app2 的项目结构一致,如下:

|-- app1                                      |--app2
    |-- package.json                             |-- package.json
    |-- webpack.config.js                        |-- webpack.config.js 
    |-- public                                   |-- public 
    |   |-- index.html                               |-- index.html
    |-- src                                      |-- src
        |-- App.js                                   |-- App.js
        |-- Button.js                                |-- Button.js
        |-- bootstrap.js                             |-- bootstrap.js 
        |-- index.js                                 |-- index.js

在 App.js 中,要动态加载其他应用的组件,如下:

const RemoteButton = React.lazy(() => import("app2/Button"));  // app1 使用 app2 的 button 组件
// const RemoteButton = React.lazy(() => import("app1/Button")); // app2 使用 app1 的 button 组件

启动 app1、app2,如下:

观察示例运行图,我们可以发现 app1 成功加载并渲染了 app2 提供的 Button 组件,并且 Button 组件和 app1 共用同一个 react、react-dom。 app2 的情形和 app1 一致。

能实现上述功能,关键在于 webpack.config.js 中 module federation 的相关配置,具体如下:

const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  ...
  plugins: [
    new ModuleFederationPlugin({
      name: "app1", // 应用 app1 的对应配置
      // name: 'app2'  // 应用 app2 的对应配置
      filename: "remoteEntry.js",
      remotes: {
        app2: "app2@http://localhost:3002/remoteEntry.js",  // 应用 app1 的对应配置
        // app1: "app1@http://localhost:3001/remoteEntry.js" // 应用 app2 的对应配置
      },
      exposes: {
        "./Button": "./src/Button",
      },
      shared: {
        "react": {
          singleton: true,
        },
        "react-dom": {
          singleton: true,
        },
      },
    }),
    ...
  ],
};

在解释 ModuleFederationPlugin 的配置项前,我们先声明两个名称:host 应用remote 应用

host 应用,使用其他应用提供的组件;remote 应用,提供组件给其他应用使用。

示例 /automatic-vendor-sharing/app1 中,app1 使用了 app2 提供的 Button 组件,那么 app1 是 host 应用,app2 是 remote 应用。反过来,示例 /automatic-vendor-sharing/app2 中, app2 使用了 app1 提供的 Button 组件,那么 app2 是 host 应用,app1 是 remote 应用。

一个应用,即可以是 host 应用,也可以是 remote 应用。

ModuleFederationPlugin 的配置项中,关键配置项为 exposesfilenameremotesshared

exposes 配置项,定义了 remote 应用可供 host 应用使用的组件。这些可供 host 应用 使用的组件会被独立打包,并生成一个供 host 应用使用的入口文件 - remoteEntry.js。(入口文件的命名,由 filename 配置项决定)。

remotes 配置项,定义了哪些 remote 应用的组件可以被 host 应用使用。host 应用使用 remote 应用提供的组件时,需要加载 remote 应用打包生成的 remoteEntry.js 文件。

shared 配置项,定义了 host 应用remote 应用组件之间可共享的依赖。

module federation 工作原理浅析

通过上节,我们知道了 module federation 的用法,接下来我们再来了解一下 module federation 的工作原理。

为了能更好的向大家说明 module federation 的工作原理,本节会以下面列出的维度来逐步进行分析:

  • 常见的 webpack 构建应用结构;
  • 一个常见的 webpack 构建是如何工作的;
  • 使用 module federation 的 webpack 构建结构;
  • 使用 module federation 的 webpack 构建是如何工作的;

常见的 webpack 构建应用

首先,我们先来了解一下一个常见的 webpack 构建应用是什么样子的。

webpack 在构建过程中,会以 entry 配置项对应的入口文件为起点,收集整个应用中需要的所有模块,建立模块之间的依赖关系,生成一个模块依赖图。然后再将这个模块依赖图,切分为多个 chunk,输出到 output 配置项指定的位置。

通常情况下,一个完整的 webpack 构建,它的结构一般如下:

最后的构建内容包括:main-chunk 和 async chunk。其中, main-chunk 为入口文件(通常为 index.js) 所在的 chunk,内部包含 runtime 模块、index 入口模块、第三方依赖模块(如 react、react-dom、antd 等)和内部组件模块(如 com-1、com-2 等);async-chunk 为异步 chunk,内部包含需要异步加载(懒加载)的模块。

示例 automatic-vendor-sharing 中的两个应用 app1、app2, 如果未使用 module federation 功能,那他们的 webpack 构建如下:

看上图,我们会发现图2和图1的结构不太一样。这是因为在 /automatic-vendor-sharing/app1 项目的 index.js 中,是通过 import("./bootstrap") 的方式导入 bootstrap.js 的,导致会生成一个包含 bootstrap、App、Button、React、React-dom 的 async chunk。webpack 在打包过程中,如果发现 async-chunk 中有第三方依赖(即 react、react-dom),则会把第三方依赖从 async-chunk 中分离,构建一个新的 vendors-chunk。

一个常见的 webpack 构建如何工作

一个通过 webpack 构建的应用启动时,chunk 的加载顺序为 main-chuk、async-chunk。

相信看过 webpack 构建结果的同学,对下面的构建结果一定不会陌生。在这里,我们还是以 /automatic-vendor-sharing/app1 去掉 module federation 的构建结果为例,来简单了解一下 webpack 构建的工作过程。

截取的部分构建代码如下:

// main-chunk
(() => {
    var __webpack_modules__ = {
        ...
    };
    var __webpack_module_cache__ = {};
    function __webpack_require__(moduleId) {
    	if (__webpack_module_cache__[moduleId]) {
            return __webpack_module_cache__[moduleId].exports;
        }
        var module = __webpack_module_cache__[moduleId] = {
            exports: {}
        }
        __webpack_module__[moduleId](module, module.exports, __webpack_require);
        return module.exports;
    }
    
    ...
    
    __webpack_require__.l = (url, done, key, chunkId) => { ... }
    
    ...
    
    __webpack_require__.f.j = (chunkId, promises) => { ... }
    
    ...
    
    var installedChunks = { ... }
    
    var webpackJsonpCallback = (parentChunkLoadingFunction, data) => { ... }
    
    var chunkLoadingGlobal = self["webpackChunk_automatic_vendor_sharing_app1"] = self["webpackChunk_automatic_vendor_sharing_app1"] || [];
    chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
    chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
    
    ...
    
    Promise.all(/* import() */[__webpack_require__.e(935), __webpack_require__.e(902)]).then(__webpack_require__.bind(__webpack_require__, 902));
})()

// vendors-chunk
(self["webpackChunk_automatic_vendor_sharing_app1"] = self["webpackChunk_automatic_vendor_sharing_app1"] || []).push([[935],{
   "./node_modules/react-dom/cjs/react-dom.development.js": (__unused_webpack_module, exports, __webpack_require__) => { ... },
    ...

}])
// async-chunk
(self["webpackChunk_automatic_vendor_sharing_app1"] = self["webpackChunk_automatic_vendor_sharing_app1"] || []).push([[902],{
    "./src/App.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => { ... },
    "./src/Button.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => { ... },
    "./src/bootstrap.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => { ... },
}])

在上面的构建代码中,有几个关键定义需要重点关注,他们依次是 __webpack_modules____webpack_module_cache____webpack_require__installedChunkswebpackJsonpCallback__webpack_require__.l 等。

  • __webpack_modules__

    __webpack_modules__ 是一个对象,用于缓存模块的执行方法。key 值对应模块的 id, value对应模块的执行方法。

    webpack 在打包时,会把每一个模块处理为一个执行方法。当运行这个执行方法时,会执行模块对应的相关代码,并返回模块的输出。

  • __webpack_module_cache__

    __webpack_module_cache__ 是一个对象,用于缓存模块的输出。key值对应模块的 id,value 对应模块的输出。

  • __webpack_require__

    __webpack_require__是一个方法,用于获取模块的输出。源代码中的 "import xx from 'xxx'",最终会被转化为 __webpack_require__(...) 的形式。

    __webpack_require__方法在获取模块输出时,会先根据模块 id 在__webpack_module_cache 中查找对应的输出。如果没有找到,就去 __webpack_modules__ 中获取模块的执行方法,然后运行模块的执行方法,获取模块的输出并缓存到 __webpack_module_cache 中。

  • installedChunks

    installedChunks 是一个对象,用于缓存 chunk。key 值对应 chunk 的 id,value 为 0。

  • webpackJsonpCallback

    webpackJsonpCallback 是一个挂载到全局变量(window、global、self) 上的全局方法,用于安装 chunk。这个方法执行时,会把 chunk 内部包含的模块及模块的执行方法收集到 __webpack_modules__ 中。

  • __webpack_require__.l

    __webpack_require__.l 是一个方法,用于加载 async-chunk。__webpack_require__.l 会根据 async-chunk 对应的 url,通过动态添加 script 的方式,获取 async-chunk 对应的 js 文件,然后执行。

这些关键点,构成了 main-chunk 的 runtime 模块。

了解完上述几个关键点之后,我们再来简单归纳一下 webpack 构建的整个工作过程:

  1. 打开应用,加载 main-chunk 对应的 js 文件;
  2. 执行 main-chunk.js, 定义 __webpack_modules__、__webpack_module_cache__、__webpack_require__、installedChunks、webpackJsonpCallback、__webpack_require__.l 等;
  3. 执行入口模块 - index 对应的代码;
  4. 如果遇到依赖模块,通过 __webpack_require__ 方法获取依赖模块的输出(先从 __webpack_module_cache 中获取,如果没有,运行 __webpack_modules__ 中模块对应的执行方法 );
  5. 如果遇到懒加载模块,通过 __webpack_modules__.l 方法获取对应的 async-chunk 并执行,然后获取相应的输出;

关于 webpack 构建的工作过程,本文只做简单讲解,之后会单出一篇文章做详细分析。

使用 module federation 的 webpack 构建

app1、app2 使用 module federation 功能以后,对应的 webpack 构建如下:

对比图 2,我们发现使用 module federation 功能以后,打包文件发生了变化。

在新的打包文件中,我们发现新增了 remoteEntry-chunk、包含 button 模块的 expose-chunk 以及包含 react 模块的 shared-chunk1 和 包含 react-dom 模块的 shared-chunk2。

remoteEntry-chunk 内部主要包含 runtime 模块,expose-chunk 中包含可被 host 应用使用的 button 组件

为什么会这样呢?

出现上述情况,主要和 ModuleFederationPlugin 的 exposes、shared 配置项有关。

首先是 exposes 配置项。exposes 配置项定义了 remote 应用的哪些组件可以被 host 应用使用。webpack 打包时,会根据 exposes 配置项,生成一个 remoteEntry-chunk 和多个 expose-chunk。应用启动以后,host 应用就可以根据 remotes 配置项指定的 url 去加载 remote 应用的 remoteEntry-chunk 和 expose-chunk。

其次是 shared 配置项。shared 配置项定义了 host 应用和 remote 应用的 expose-chunk 之间可共享的依赖。shared 指定了共享的依赖为 react、react-dom,因此 react 模块和 react-dom 模块会被分离为新的 shared-chunk。

使用 module federation 的 webpack 构建如何工作

module federation 主要提供了两个功能:应用间组件互用、host 应用和 remote 应用组件的依赖共享。那这两个功能时如何实现的呢?

我们先看应用间组件互用的实现逻辑。

通过图3 的 webpack 构建结果,我们基本可以猜到 module federation 实现应用间组件互用的原理。

很简单,就是 remote 应用生成一个 remoteEntry-chunk 和多个 expose-chunk。 host 应用启动后,通过 remotes 配置项指定的 url,去动态加载 remote 应用的 remoteEntry-chunk 和 expose-chunk,然后执行并渲染 remote 应用的组件。

接下来,我们再看 host 应用和 remote 应用组件的依赖共享的实现逻辑。

一个常见的 webpack 构建如何工作 的构建结果中,我们可以发现多个 webpack 构建之间其实是相互隔离、无法互相访问的。那么 module federation 是如何打破这种隔离的呢?

答案是 sharedScope - 共享作用域。应用启动以后, host 应用和 remote 应用的 remote-chunk 之间会建立一个可共享的 sharedScope,内部包含可共享的依赖。

使用 module federation 功能以后,webpack 会在 app1 应用的 main-chunk 的 runtime 模块中添加一段逻辑:

var __webpack_modules__ = {
	...
    "webpack/container/reference/app2": (module, __webpack_exports, __webpack_require__) => {
         ...
         module.exports = new Promise((resolve, reject) => {
	     ...
	     __webpack_require__.l("http://localhost:3002/remoteEntry.js", (event) => {...}, "app2");
       }).then(() => app2);
    }
}

...

(() => {
    __webpack_require__.S = {};
    
    __webpack_require__.I = (name, initScope) => {
    	...
        var scope = __webpack_require__.S[name];
        ...
        var register = (name, version, factory) => {
            var versions = scope[name] = scope[name] || {};
            var activeVersion = versions[version];
            if(!activeVersion || !activeVersion.loaded && uniqueName > activeVersion.from) versions[version] = { get: factory, from: uniqueName }
        }
        
        var initExternal = (id) => {
            ...
            var module = __webpack_require__(id);
            if(!module) return;
            var initFn = (module) => module && module.init && module.init(__webpack_require__.S[name], initScope);
            if(module.then) return promises.push(module.then(initFn, handleError));
            var initResult = initFn(module);
            if(initResult && initResult.then) return promises.push(initResult.catch(handleError));
        }
        
        ...
        
        switch(name) {
            case "default": {
                register("react-dom", "17.0.1", () => Promise.all([__webpack_require__.e("vendors-node_modules_react-dom_index_js"), __webpack_require__.e("webpack_sharing_consume_default_react_react-_2997")]).then(() => () => __webpack_require__(/*! ./node_modules/react-dom/index.js */ "./node_modules/react-dom/index.js")));
                register("react", "17.0.1", () => Promise.all([__webpack_require__.e("vendors-node_modules_react_index_js"), __webpack_require__.e("node_modules_object-assign_index_js")]).then(() => () => __webpack_require__(/*! ./node_modules/react/index.js */ "./node_modules/react/index.js")));
                initExternal("webpack/container/reference/app2");
         }
         break;
      }
      ...
   }
    
})()

针对上面 runtime 模块中新增的逻辑,我们先做一下简单的讲解:

  • __webpack_modules__ 中新增了 "webpack/container/reference/app2" 模块及对应的执行方法。

    运行执行方法时,会动态加载 app2 的 remoteEntry.js。

  • __webpack_require__ 新增了 S 对象。

    S 是一个普通对象,用于存储 app1 中 shared 配置项指定的共享内容。

    /automatic-vendor-sharing/app1 工作过程中, S 的结构如下:

    {
        default: {
            'react': {
                from: '@automatic-vendor-sharing/app1',
                get: () => {...}
            },
            'react-dom': {
                from: '@automatic-vendor-sharing/app1',
                get: () => {...}
            }
        }
    }
    

    其中, default 是默认的 sharedScope 的名称;react、react-dom 对应 shared 配置项中的共享依赖;from 为共享依赖实际取自哪个应用;get 为共享依赖的获取方法。

  • __webpack_require__ 新增了 I 方法。

    __webpack_require__.I 方法用于初始化 app1 的 __webpack_require__.S 对象。

    __webpack_require__.I 内部定义了两个关键方法:register 和 initExternal 。 其中, register 执行时,会将 shared 配置项指定的共享依赖及获取方法添加到 __webpack_require__.S 中;initExternal 执行时,会加载 app2 提供的 remoteEntry.js,然后使用 app1 应用的 __webpack_require__.S 来初始化 app2 应用的 __webpack_require__.S 对象。

而 app2 应用的 remote-chunk 的代码片段如下:

var app2 = (() => {
    ...
    var __webpack_modules = ({
        "webpack/container/entry/app2": (__unused_webpack_module, exports, __webpack_require__) => {
          ...
          var get = (module, getScope) => {...}
          var init = (shared, initScope) => {
              if (!__webpack_require__.S) return;
              var oldScope = __webpack_require__.S["default"];
              var name = "default";
              ...
              __webpack_require__.S[name] = shareScope;
	          return __webpack_require__.I(name, initScope);
          }
          __webpack_require__.d(exports, {
              get: () => get;
              init: () => init
          });
        }
    
    })
    
    ... // 中间部分和 app1 的 main-chunk 的 runtime 基本相同
    
    return __webpack_require("webpack/container/entry/app2")

})()

上述代码执行时,会返回一个全局变量 - app2。 app2 中包含一个 init 方法,这个方法会使用 app1 的 __webpack_require__.S 来初始化 app2 的 __webpack_require__.S。

在初始化的时候,会对比 app1 、app2 中 react、react-dom 的版本,将 shareScope 中的共享依赖更新为两个应用里面较新的版本。

即如果 app1 中 react 的版本高于 app2, 那么 __webpeck_require__.S[default].react的 from 和 init 属性对应 app1,实际使用时 react 是从 app1 中获取;反之__webpeck_require__.S[default].react的 from 和 init 属性对应 app2,实际使用时 react 是从 app2 中获取.

综上所述,我们针对依赖共享的实现逻辑,做一个简单梳理:

  1. 打开应用 app1, 加载 main-chunk 对应的 js 文件;
  2. 执行 main-chunk.js, 定义 __webpack_modules__、__webpack_module_cache__、__webpack_require__、installedChunks、webpackJsonpCallback、__webpack_require__.l、__webpack_require__.S、__webpack_require__.I 等;
  3. 执行入口模块 index.js 对应的代码。执行时,会触发 __webpack_require__.I 的执行;
  4. __webpack_require__.I 方法开始执行,首先通过 register 方法初始化 __webpack_require__.S 对象,然后通过 initExternal 方法去获取 app2 的 remoteEntry.js。获取 app2 的 remoteEntry.js 时,会返回一个 promise 对象,当 app2 的 remoteEntry.js 加载并执行完毕时, promise 的状态才会变为 resolved;
  5. 动态加载 app2 应用 的 remoteEntry.js 并执行,返回一个包含 get、init 的全局变量 app2;
  6. 步骤 4 中的 promise 对象的状态变为 resolved,注册的 callback 触发,开始执行 app2.init;
  7. app2.init 方法执行,使用 app1 应用的 __webpack_require__.S 初始化 app1 应用的 __webpack_require__.S。

这样便实现了依赖共享。

应用启动以后, app1 的结构如下:

module federation 配置项详解

本节, 我们主要来详细了解一下 module federaion 的配置项。

一个完整的 module federation 配置项,包含内容如下:

new ModuleFederationPlugin({
    name: 'xxx',
    filename: 'xxx',
    library: {
        type: 'xxx',
        name: 'xxx'
    },
    remotes: {
        app2: 'app2@xxxx',
        app3: 'app3@xxxx',
        ...
    },
    exposes: {
        './Button': './src/Button',
        ...
    },
    shared: {
        'react': {
            import: 'xxx',
            singleton: true,
            requiredVersion: 'xxx',
            strictVersion: 'xxx',
            shareScope: 'xxx',
            packageName: 'xxx',
            sharedKey: 'xxx',
            eager: true
        }
    },
    shareScope: 'xxx'
})
  • name

    当前应用的别名。

  • filename

    入口文件名, remote 应用供 host 应用消费时,remote 应用提供的远程文件的文件名。

  • exposes

    remote 应用被 host 应用消费时,有哪些输出内容可以被 host 应用使用。

    exposes 是一个对象, key 为输出内容在 host 应用中的相对路径,value 为输出内容的在当前应用的相对路径(也可以是绝对路径)。

    new ModuleFederationPlugin({
    	...
        exposes: {
        	'./Button': '../src/component/Button'
        }
    })
    

    注意:如果我们在 host 应用中是 import('app2/Button'), 那么 exposes 中的 key 必须为 './Button'; 如果是 import('app2/shared/Button'), 那么 exposes 中的 key 必须为 './shared/Button'.

  • library

    library 定义了 remote 应用如何将输出内容暴露给 host 应用。

    配置项的值是一个对象,如 { type: 'xxx', name: 'xxx'}。

    其中,name,暴露给外部应用的变量名; type,暴露变量的方式。

    type 的值,和 output.libraryTarget 的值类似:

    • var: remote 的输出内容分配给一个通过 var 定义的变量;

      var app2; app2 = (() => {
          ...
          return __webpack_require__(...);
      })();
      
    • assign: remote 的输出内容分配给一个不通过 var 定义的变量;

      app2 = (() => {
          ...
          return __webpack_require__(...);
      })();
      
    • this: remote 的输出内容作为当前上下文 this 的一个属性,属性名为 name 对应的值;

      this["app2"] = (() => {
          ...
          return __webpack_require__(...);
      })()
      
    • window: remote 的输出内容作为 window 对象的一个属性,属性名为 name 对应的值;

      window["app2"] = (() => {
          ...
          return __webpack_require__(...);
      })()
      
    • self: remote 的输出内容作为 self 对象的一个属性,属性名为 name 对应的值;

      self["app2"] = (() => {
          ...
          return __webpack_require__(...);
      })();
      
    • commonjs: remote 的输出内容作为 exports 的一个 属性,属性名为 name 对应的值;

      exports["app2"] = (() => {
          ...
          return __webpack_require__(...);
      })();
      
    • commonjs2: remote 的输出内容作为 module.exports 的一个 属性,属性名为 name 对应的值;

      module.exports["app2"] = (() => {
          ...
          return __webpack_require__(...);
      })();  
      
    • amd: remoteEntry.js 符合 AMD 规范;

      define('app2', [], function() { return (() => {...})()});
      
    • umd: remoteEntry.js 符合 UMD 规范;

      (function(root, factory){
          if(typeof exports === 'object' && typeof module === 'object')
              module.exports = factory();
          else if(typeof define === 'function' && define.amd)
              define([], factory);
          else if(typeof exports === 'object')
              exports["app2"] = factory();
          else
              root["app2"] = factory();
      }(window, function() {
          return (() => {...})()}
      )
      
    • jsonp: 将 remote 的输出内容包裹到一个 jsonp 包装容器中;

      app2((() =>{...})())
      
    • system: remoteEntry.js 符合 Systemjs 规范;

      System.register("app2", [], function(__WEBPACK_DYNAMIC_EXPORT__, __system_context__) {
          return {
              execute: function() {
      	    __WEBPACK_DYNAMIC_EXPORT__(...)
              }
          }
      }
      

    常用的 type 类型为:var、window,默认为 var。

  • remotes

    被当前 host 应用消费的 remote 应用。

    remotes 是一个对象,key 值是一个要消费的应用的别名。如果我们在要 host 应用中使用 remote 应用的 button 组件时,我们的代码如下:

    const RemoteButton = React.lazy(() => import('app2/button));
    

    其中, import url 中的 app2 对应 remotes 配置项中的 key 值。

    value 为 remote 应用的对外输出及url,格式必须严格遵循: 'obj@url'。其中, obj 对应 remote 应用中 library 中的 name 配置项, url 对应 remote 应用中 remoteEnter 文件的链接。

  • shared

    shared 配置项指示 remote 应用的输出内容和 host 应用可以共用哪些依赖。 shared 要想生效,则 host 应用和 remote 应用的 shared 配置的依赖要一致。

    • import

      共享依赖的实际的 package name。

      {
      	...,
          shared: {
          	'react-shared': {
                  import: 'react'
              }
          }
      }
      

      如果未指定,默认为用户自定义的共享依赖名,即 react-shared。如果是这样的话,webpack 打包是会抛出异常的,因为实际上并没有 react-shared 这个包。

    • singleton

      是否开启单例模式。如果值为 true,开启单例模式;值为 false,不开启单例模式。

      默认值为 false,即不开单例模式

      如何启用单例模式,那么 remote 应用组件和 host 应用共享的依赖只加载一次,且与版本无关。 如果版本不一致,会给出警告。

      加载的依赖的版本为 remote 应用和 host 应用中,版本比较高的。

      不开启单例模式下,如果 remote 应用和 host 应用共享依赖的版本不一致,remote 应用和 host 应用需要分别各自加载依赖。

    • requiredVersion

      指定共享依赖的版本,默认值为当前应用的依赖版本。

      如果 requiredVersion 与实际应用的依赖的版本不一致,会给出警告。

    • strictVersion

      是否需要严格的版本控制。

      单例模式下,如果 strictVersion 与实际应用的依赖的版本不一致,会抛出异常。

      默认值为 false。

    • shareKey

      共享依赖的别名, 默认值值 shared 配置项的 key 值.

    • shareScope

      当前共享依赖的作用域名称,默认为 default。

      在 xxx 中,我们了解到项目运行时, __webpack_require__.S 的结构为:

      {
          default: {
              'react': {
                  from: '@automatic-vendor-sharing/app2',
                  get: () => {...}
              },
              'react-dom': {
                  from: '@automatic-vendor-sharing/app2',
                  get: () => {...}
              } 
          }
      }
      

      __webpack_require__.S["default"] 中的 default 即为 shareScope 指定的 default。

    • eager

      共享依赖在打包过程中是否被分离为 async chunk。

      eager 为 false, 共享依赖被单独分离为 async chunk; eager 为 true, 共享依赖会打包到 main、remoteEntry,不会被分离。

      默认值为 false,如果设置为 true, 共享依赖其实是没有意义的。

  • shareScope

    所用共享依赖的作用域名称,默认为 default。

    如果 shareScope 和 share["xxx"].shareScope 同时存在,share["xxx"].shareScope 的优先级更高。

其他

  1. 为什么 index.js 中需要以 import() 的方式引入 bootstrap.js ?

    在学习 module federation 的时候,对示例中 index.js 中使用 import() 引入 bootstrap.js 的写法非常不理解,然后就把 bootstrap.js 中的内容,直接粘贴到 index.js 中。启动时,发现报错,报错如下:

    看了一下打包以后的代码,发现出现上述问题,是因为 react 模块的执行方法运行的时间不对。

    使用 module federation 以后,react 会作为异步模块,在 app1 的 main.js、app2 的 remote.js 完成加载并且初始化 sharedScope 以后,才会加载并运行对应的执行方法。

    如果在 index.js 中直接通过 import React from 'react' 的方式引入 react,那么 react 会作为静态依赖打包到 main-chunk 中。应用启动以后,先执行 runtime 模块的代码,然后开始执行入口文件 index.js 的代码。此时,会立即运行 react 的执行方法。运行执行方法的时间不对,代码就抛出了上述异常。

  2. module federation 是否可以做到与技术栈无关?

    答案是可以的。

    假设两个应用, host 应用使用 react 技术栈, remote 应用使用 vue 技术栈,host 应用在使用 remote 应用提供的组件时,不能直接使用,需要额外执行 vue.mount('#xxx') 方法,将 remote 组件挂载的指定位置。

  3. 共享依赖的版本控制

    module federation 在初始化 shareScope 时,会比较 host 应用和 remote 应用之间共享依赖的版本,将 shareScope 中共享依赖的版本更新为较高版本。

    在加载共享依赖时,如果发现实际需要的版本和 shareScope 中共享依赖的版本不一致时,会根据 share 配置项的不同做相应处理:

    • 如果配置 singleton 为 ture,实际使用 shareScope 中的共享依赖,控制台会打印版本不一致警告;
    • 如果配置 singleton 为 ture,且 strictVersion 为 ture,即需要保证版本必须一致,会抛出异常;
    • 如果配置 singleton 为 false,那么应用不会使用 shareScope 中的共享依赖,而是加载应用自己的依赖;

    综上,如果 host 应用和 remote 应用共享依赖的版本可以兼容,可将 singleton 配置为 ture;如果共享依赖版本不兼容,需要将 singleton 配置为 false。

  4. 多个应用(超过 2 个) 是否可共用一个 shareScope ?

    假设有这么一个场景, 三个应用 - app1、app2、app3, app2 是 app1 的 remote 应用, app3 是 app2 的 remote 应用, 那么他们是否可共用一个 shareScope ?

    答案是可以的。

    使用 module federation 功能以后,所有建立联系的应用,共用一个 shareScope。

总结

到这里,module federation 的说明就已基本结束,相信大家对 module federation 的用法及工作原理已经有了一个比较深入的了解了吧。由于本人水平有限,有些地方可能没有理解说明到位。如果有疑问或者错误,欢迎大家留言指正,一起学习,共同进步。

最后,我们做一个总结:

  1. 使用 module federation,我们可以在一个应用中动态加载并执行另一个应用的代码,且与技术栈无关;
  2. 通过 module federation 建立连接的应用,共享同一个 shareScope,可实现依赖共享;
  3. host 应用的入口文件,必须使用 import() 的方式,否则会报错;
  4. 使用 module federation 需要基于 webpack5;

参考文档

完成本文,参考文档如下: