一文通透讲解webpack5 module federation

21,313 阅读19分钟

写在前面:转眼间和大家见面都是在新的一年了,先祝大家在新的一年里顺风顺水呀;2021年挑战和成长都太多了,今天和大家分享一篇自己调研的知识~ 本文通篇阅读大概需要20分钟左右 Module federation 英:[ˌfedəˈreɪʃn],本文从模块联邦产生起源说起,由作者出发渐进式阐述了模块联邦的使用,通过产物进行赘述其运行的原理,再从源码描述了模块联邦的实现,同时也阐述了模块联邦实现微前端时的方案。

走近作者,提出问题

基于作者的问答,了解作者基于什么背景去提出问题,并期望模块联邦能够带来什么样的功能? 原文: github.com/webpack/web… “我”指的是以作者第一口吻进行描述

  • 我希望能够从其他地方托管的项目中 Webpack 包中的模块
  • 不想使用者去担心跨系统同步它们
    • 类似于npm格式,当包升级时候,依赖的repo仓库需进行更改升级
  • 我不想要实现比较特殊,类似于普通的chunk
  • 我想从另一个构建加载代码,比如动态导入和代码拆分
  • 我希望多个构建看起来和感觉像一个monolith
    • monolith git仓库管理的方式,言简意赅模块联邦不会在被调用方比较特殊
  • 我能够独立部署前端应用程序,并期望 Webpack 在运行时进行编排(微前端)
  • 我不想使用集成度较低/面向框架的解决方案, 例如 SingleSPA、浏览器事件、service workers,我想使用 Webpack 作为外部块和模块的宿主容器,不是加载其他应用程序入口点,而是加载其他应用程序块并使用这些模块
  • 如果需要,整个公司应该能够跨每个 UI 联合代码,从面向用户到后端。有效地将多构建、多团队、多mono-repo变成浏览器中的一个 SPA,无需重新加载页面或下载包含大部分相同节点模块的附加代码和包 总结:期望模块联邦的是非特殊性的,具备独立功能且能够动态加载和引入的可复用性较强的功能;

关于模块联邦,提出我们的疑问

  • 是什么?应用场景,解决的问题是什么
  • 是如何使用的呢?
  • 多个模块是如何运行的 带着我们的问题,深入浅出了解 module federation

模块联邦的概述

前端开发的痛点

只针对资源的复用与构建编译方面 在团队项目中,有AB两个项目针对于项目的一些公共模块,在进行组件/方法复用的时候,B项目抽离出公共的模块打包成npm包,通过发包的方式进行进行组件/方法的复用,当组件(方法)有一些更新时候,尤其是更改了某些存在的bug,就不得不通知依赖模块进行升版;如果存在多个依赖方,这种“发布-> 通知-> 更改”模式无疑是低效率的;

image.png

尤其是作为大型的公共模块,在进行升版时,升级1-2个月也是常有的事情
举个实际例子:

  • 升级公共的方法时候,请求库升级,,通知子应用升级,耗时1-2个月也是有可能的通过主应用注入请去方法,实现版本的统一管理
import {EAxiosRequest} from 'xxxxx'
// 最终改造后
const requestBridge: TRequestBridge = window.FBridge?.requestBridge;
export const AxiosRequest = requestBridge.AxiosRequest || EcopAxiosRequest;
  • 编译速度减慢,业务多,模块多,影响到编译速度问题的因素有很多种,不仅仅是模块多,还有三方的模块依赖等,
  • 初始的编译 image.png

  • 项目大的编译 image.png

当项目过大的时候,编译时长很长通常影响因素有很多,但是对于一些公共未改动的模块,每次进行编译都是非常浪费资源的,对于此类模块,避免重复打包是提高编译速度的重要一步

模块联邦是什么

www.bram.us/2020/03/12/…

  • 是Webpack 5 的新特性之一,允许在多个 webpack 编译产物之间共享模块、依赖、页面甚至应用
  • 提供了一种轻量级的、在运行时,通过全局变量组合,在不同模块之前进行数据的获取
  • 提供了一种解决应用集的官方方案。 每个构建都充当一个容器,也可将其他构建作为容器。通过这种方式,每个构建都能够通过从对应容器中加载模块来访问其他容器暴露出来的模块。
  • host:在页面加载过程中(当 onLoad 事件被触发)最先被初始化的 Webpack 构建;
  • remote:部分被 “host” 消费的另一个 Webpack 构建;
  • bidirectional(双向的) hosts:当一个 bundle 或者 webpack build 作为一个 host 或 remote 运行时,它要么消费其他应用,要么被其他应用消费——均发生在运行时(runtime)。

如何使用模块联邦

配置

Remote(提供者模块)

const { ModuleFederationPlugin } = require('webpack').container;
 new ModuleFederationPlugin({
      // 应用名,全局唯一,不可冲突。 
      name: "component_app",
      // 暴露的文件名称 
      filename: "remoteEntry.js",
      // 远程应用暴露出的模块名。
      exposes: {
        "./Button": "./src/Button.jsx",
        "./TimeShow": "./src/TimeShow.jsx",
      },
      // 依赖包 依赖的包 webpack在加载的时候会先判断本地应用是否存在对应的包,如果不存在,则加载远程应用的依赖包。
      shared: {
        react: {
          singleton: true
        },
        moment: {
          singleton: true
        }
      }
    }),`

host(使用者模块)

 new ModuleFederationPlugin({
      //远程访问地址入口 
      remotes: {
        "component_app": "component_app@http://localhost:3001/remoteEntry.js",
        "util_app": "util_app@http://localhost:3003/remoteEntry.js",
      }, 
    }),

使用

import Button from 'component_app/Button';
<Button></Button>

属性配置

new ModuleFederationPlugin({
 name: "app-1", // 暴露出去的name名称
 library: { type: "var", name: "app_1" }, // 
 filename: "remoteEntry.js",
 // 
 remotes: {
    app_02: 'app_02@xxx.js',
    app_03: 'app_03@xxx.js',  
},
  exposes: {
    antd: './src/antd',
    button: './src/button',  
},
  shared: ['react', 'react-dom'],
}),

name

必须,唯一 ID(webpack ModuleFederationPlugin 届的身份证),作为输出的模块名,使用的时通过 name/{name}/{expose}的方式使用;

library

library 定义了 remote 应用如何将输出内容暴露给 host 应用。配置项的值是一个对象,如 { type: 'xxx', name: 'xxx'}。其中,name,暴露给外部应用的变量名; type,暴露变量的方式。和 output.libraryTarget 差不多

 library: { type: 'var', name: 'com' }

更多的是输出规范,方便更多的模块进行使用,library对应的值

  • var remote 的输出内容分配给一个通过 var 定义的变量;
  • assign remote 的输出内容分配给一个不通过 var 定义的变量;
  • thisremote 的输出内容作为当前上下文 this 的一个属性,属性名为 name 对应的值
  • window remote 的输出内容作为 window 对象的一个属性,属性名为 name 对应的值;
  • self remote 的输出内容作为 self 对象的一个属性,属性名为 name 对应的值;
  • amd / umd 生成 amd/cmd的格式
  • commonjs/ commonjs2打包成commonjs形式

filename

暴露出去的文件名字,此时好听的名字不如符合场景的名字 remoteEntry romoteInit romoteIndex

remotes

可选,表示作为 Host 时,去引用哪些 Remote

   remotes: {
        "component_app": "component_app@http://localhost:3001/remoteEntry.js",
        "util_app": "util_app@http://localhost:3003/remoteEntry.js",
      }, 

exposes

可选,表示作为 Remote 时,export 哪些属性被消费;

 exposes: {
    './useInArray': './package/use-in-array.js',
    './util': './package/index.js',
   },
  • 如果我们在 host 应用中是 import('app2/useInArray'), 那么 exposes 中的 key 必须为 './Button'; 如果是 import('app2/xx/util'), 那么 exposes 中的 key 必须为 './xx/Button'.

shared

可选: shared 配置项指示 remote 应用的输出内容和 host 应用可以共用哪些依赖。 shared 要想生效,则 host 应用和 remote 应用的 shared 配置的依赖要一致。 shared的设置有利于当前exposes的组件/方法和当前host引用的依赖保持一致,同时也能够减少不必要的开销;“求近策略”
remotes 将会首先依赖来自 host 的依赖,如果 host 没有依赖,它将会下载自己的依赖。没有代码层面的冗余,而只有内置的冗余
Module Federation 使 JavaScript 应用得以从另一个 JavaScript 应用中动态地加载代码 —— 同时共享依赖。如果某应用所消费的 federated module 没有 federated code 中所需的依赖,Webpack 将会从 federated 构建源中下载缺少的依赖项。

Import

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

shared: { 
    // 别名 
    'react-shared': { 
        // 实际导出名字 
        import: 'react', 
   }, 
},
requiredVersion

指定共享依赖的版本,默认值为当前应用的依赖版本。 如果 requiredVersion 与实际应用的依赖的版本不一致,会给出警告

strictVersion

是否需要严格的版本控制。
单例模式下,如果 strictVersion 与实际应用的依赖的版本不一致,会抛出异常。 默认值为 false。

shareKey

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

shareScope

当前共享依赖的作用域名称,默认为 default。 在 xxx 中,我们了解到项目运行时, webpack_require.S 的结构为:
webpack_require.S["default"] 中的 default 即为 shareScope 指定的 default。

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

共享依赖在打包过程中是否被分离为 async chunk。 eager 为 false, 共享依赖被单独分离为 async chunk; eager 为 true, 共享依赖会打包到 main、remoteEntry,不会被分离。
默认值为 false,如果设置为 true, 共享依赖其实是没有意义的。

运行原理分析

webpack5将ModuleFederationPlugin这个插件封装在内部,说明其本身的功能早就在webpack计划中,而不是通过后期单独插件的方式去扩大功能,同时也能够体现此插件在代码复杂度,实现流程上而言,是能够和正常的编译流程在一起的; ps: 图源网络,在参考文中标出
通过图进行分析,我们有两个模块,本地host模块和远程remote模块,remote模块可暴露一些组件供host模块使用,

  • 通过share的模块回自动在执行链路过程中生成share scope,而本地host和remote都会进行提供相对应的modules, 在进行使用的时候通过校验share scope中的provided模块是否符合当前的版本等信息,如果符合则进行加载模块并存储,从而达到了共享模块的母的
  • 模块加载是通过异步加载,只有在使用的时候才会进行加载对应的模块内容,在未加载前只存储对应的相关模块属性信息

容器是由容器入口创建的,该入口暴露了对特定模块的异步访问。暴露的访问分为两个步骤

  • 加载模块: 异步加载,将在 chunk 加载期间完成
  • 执行模块: 将在与其他(本地和远程)的模块交错执行期间完成。这样一来,执行顺序不受模块从本地转换为远程或从远程转为本地的影响 容器可以嵌套使用,容器可以使用来自其他容器的模块。容器之间也可以循环依赖。
    简单了解webpack打包后的产物,通过产物去分析模块联邦打包后的具体代码,如了解请忽略这部分阐述
  • webpack_require.e 动态引入内容加载
__webpack_require__.e = (chunkId) => {
    // 形成一个作用链一样引用 
      return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
        // 加载数据内容 
        __webpack_require__.f[key](chunkId, promises);
        return promises;
      }, []));
    };
  • webpack_require.d 为每个module导出定义一个get的函数,当在你访问到这个资源的时候,就先走getter函数

   __webpack_require__.d = (exports, definition) => {
      for(var key in definition) {
        if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
          Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
        }
      }
    };
  • webpack_require.o
 (() => {
    __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  })();
  • webpack_require.S 在模块联邦中专门是使用的 存储模块
  • webpack_require.I 加载某个shareScope下的内容,
    • webpack_require.S 中是否有符合条件的模块提供使用,如果有的话,直接获取,如果没有,则加载当前所在模块下对应的模块 通过打包结果入手进行分析

动态远程容器

该容器接口支持 getinit 方法。 init 是一个兼容 async 的方法,调用时,只含有一个参数:共享作用域对象(shared scope object)。此对象在远程容器中用作共享作用域,并由 host 提供的模块填充。 可以利用它在运行时动态地将远程容器连接到 host 容器。 init.js

(async () => {
  // 初始化共享作用域(shared scope)用提供的已知此构建和所有远程的模块填充它
  await __webpack_init_sharing__('default');
  const container = window.someContainer; // 或从其他地方获取容器
  // 初始化容器 它可能提供共享模块
  await container.init(__webpack_share_scopes__.default);
  const module = await container.get('./module');
})();
  • 在加载组件前已经加载好相关资源

模块打包产物

image.png

  • main.js,应用主文件;
  • remoteEntry.js,作为 remote 时被引的文件;
  • 一堆异步加载的文件,main.js 或 remoteEntry.js 里可能加载的文件;

如何进行消费和运行呢 ?

了解如何消费就需要从两个方面去考虑

  • host内如何引用和加载
  • remote是如何进行加载
    • romote是如何存储(打包结果) 创建了三个模块
  • app 当前host执行模块
  • component_app 提供远程组件
  • util_app 提供远程方法 image.png

component_app暴露

共暴露两个组件 Button TimeShow

  exposes: {
    './Button': './src/Button.jsx',
    './TimeShow': './src/TimeShow.jsx',
  },
app中引用

只使用其中一个 组件,探究如何按需加载

// import TimeShow from 'component_app/TimeShow';
import Button from 'component_app/Button';
import useInArray from 'util_app/useInArray';

export default () => {
  const inArray = useInArray([1, 2, 3, 4, 5], 5);
  return (
    <div>
      <h1>本地启动项目</h1>
      <div>
        <h2>引用远程组件库:component_app</h2>
        <Button> </Button>
        {/* <TimeShow></TimeShow> */}
      </div>
      <div>
        <h2>引用远程方法库</h2>
        <p> [1, 2, 3, 4, 5] {`${inArray }`}</p>
      </div>
    </div>
  );
};``

加载资源

按需加载,不使用不加载,异步加载的处理原理,在于remote暴露出来的远程方法,资源加载完成后,就可自行进行处理函数

产物分析

最重要的有expose和share以及entry

exposes暴露组件

【remote】util_app

/util_app/dist/remoteEntry.js

  • 针对于exposes的内容打包后生成moduleMap的映射的promise函数,其中函数内部保存着当前资源所需要的地址内容加载
  • 当remote使用时候,通过moduleID进行加载和使用
// windows 变量
let B;

// 存储的exposes的modules的内容
const moduleMap = {  
     "./useInArray": () => {
    return __webpack_require__.e("package_use-in-array_js").then(() => (() => ((__webpack_require__(/*! ./package/use-in-array.js */ "./package/use-in-array.js")))));
  },
  "./util": () => {
    return __webpack_require__.e("package_index_js").then(() => (() => ((__webpack_require__(/*! ./package/index.js */ "./package/index.js")))));
  }
};

B = {  
   get(moduleId) {
   // 省略一些缓存等操作
    return moduleMap(moduleId);  
   }
   init(shareScope, initScope) {
        if (!__webpack_require__.S) return;
        var oldScope = __webpack_require__.S["default"];
        var name = "default"
        if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
        __webpack_require__.S[name] = shareScope;
        return __webpack_require__.I(name, initScope);
}
}
  • moduleMap:通过exposes生成的模块集合
  • get: host通过该函数,可以拿到remote中的组件
  • init:host通过该函数将依赖注入remote中
【host】app构建产物
// 1.异步获取模块 export 内容
// 2.
/* webpack/runtime/remotes loading */
()=>{
   var chunkMapping = {
      "src_bootstrap_jsx": [
        "webpack/container/remote/component_app/Button",
        "webpack/container/remote/util_app/useInArray"
      ]
    };
   // 2. 取 remote 的模块
   var idToExternalAndNameMapping = {
      "webpack/container/remote/component_app/Button": [
        "default",
        "./Button",
        "webpack/container/reference/component_app"
      ],
       // .....
    };
   // 3. 从romote中取某个单个模块
       var getScope = __webpack_require__.R;
      if(!getScope) getScope = [];
      var data = idToExternalAndNameMapping[id];
      if(getScope.indexOf(data) >= 0) return;
      getScope.push(data);
  // 4. 加载某个异步的模块

}

这其中的原理:

  • 多个 bundler 之间通过全局变量串联;如component_app远程变量会被host内进行访问
  • remote 会 export get 方法获取他的子模块,子模块的获取通过 Promise 以按需的方式引入;

shared分享模块

app如何让components_app使用share的库?

【remote】components_app 的shared
  shared: {
        react: {
          singleton: true,
        },
        moment: {
          singleton: true,
        },
      },

components_app的shared的模块

 // 存储components_app的shared  在app中是否存在
 __webpack_require__.S ={} 
 // 记录所有的share的模块
__webpack_require__.S[name] = shareScope; 
 
 // 1.加载share的模块 
  var ensureExistence = (scopeName, key) => {
       var scope = __webpack_require__.S[scopeName];
       if(!scope || !__webpack_require__.o(scope, key)) throw new Error("Shared module " + key + " doesn't exist in shared scope " + scopeName);
       return scope;
     };  
     // 初始化module流程
      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);
     });
    
  // 加载share存储模块 
     var load = /*#__PURE__*/ init((scopeName, scope, key) => {
       ensureExistence(scopeName, key);
       return get(findVersion(scope, key));
     }); 

【host】app构建产物

入口文件如何注入分享模块呢 mian.js

// 自动加载了share分享模块
Promise.all(/*! import() */[__webpack_require__.e("vendors-node_modules_react-dom_index_js"), __webpack_require__.e("src_bootstrap_jsx-webpack_sharing_consume_default_react_react")])
.then(__webpack_require__.bind(__webpack_require__, /*! ./bootstrap.jsx */ "./src/bootstrap.jsx"));

image.png

  • 先加载 main.js,加载里面的share方法
  • 再加载remote中entry.js通过remote的init方法将自身shared写入remote中,再通过get获取remote中expose的组件,而作为remote时,判断host中是否有可用的共享依赖,若有,则加载host的这部分依赖,若无,则加载自身依赖。 remote中get方法
   let 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;
      };

如何判断是使用自身还是使用remote本身 和执行的顺序有关系,入口main.js 已经注入到__webpack_require__.S 中相关可使用的模块内容,包含版本等关系,如果remote加载使用中发现__webpack_require__.S的模块不匹配,就会去加载其内部的模块;

对比 external 和DllPlugin

对于代码共享代码我们拥有的最成熟方案是 externals 或 DllPlugin

externals

  • externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖
  • external 需要自行在 html 引入相关 script,此方案只需引一个 runtime 文件,runtime 里维护了 chunk 的映射表
  • external 需要自行处理库的依赖,比如 antd 依赖 moment,那么就需要分别引 moment 和 antd 的 umd 文件,并且保证顺序

DllPlugin

  • DllPluginDllReferencePlugin 用某种方法实现了拆分 bundles,同时还大幅度提升了构建的速度。
  • 本质上算是预编译实现模块的共享

源码揭秘(可选)

我以为的Module Federation(模块联邦) 🤯 🤐,实际上的 🧐,webpack卫模块联邦做的事情处理还是蛮多的,通过作者的译文答复能够推敲出,模块联邦的实现模式和其他普通的module在本质上是无差别的,唯一有差别的是在链路的引用过程中的差异;

了解webpack的hooks和plugin

首先要了解webpack如何实现自定义插件,以及自定义插件的写法 webpack打包顺序 因为了解webpack的生命周期钩子以及暴露出来的方法是非常重要的;

  • compiler 是webpack进行读取配置时生成,全程只有一个,对象代表了完整的webpack环境配置,这个对象在启动webpack时候被一次性创建,并配置好所有可操作的设置,包括options、loader和plugin,当在webpack环境中应用一个插件时,插件将会受到此compiler对象引用,可以使用它来访问webpack的主环境;
  • compilation对象代表了一次资源版本构建,当运行webpack开发环境中间件时候,每当检测到一个文件变化,就会创建一个新的compilation,从而生成一组新的编译资源,一compilation表现了当前的模块资源,编译生成资源、变化的文件以及被跟踪的依赖状态信息compilation对象也提供了很多关键时机的回调,以供插件做自定义的处理时选择使用; 更多的钩子请了解github.com/webpack/web…
  • NormalModuleFactory Compiler 使用 NormalModuleFactory 模块生成各类模块。从入口点开始,此模块会分解每个请求,解析文件内容以查找进一步的请求,然后通过分解所有请求以及解析新的文件来爬取全部文件。在最后阶段,每个依赖项都会成为一个模块实例。扩展了 Tapable 并提供了以下的生命周期钩子;

走进源码

github.com/webpack/web…

本次源码重点不在于深究每个打包内的具体实现,主要讲述流程

/webpack/lib/container/ModuleFederationPlugin.js 主入口文件引用图,其中单个功能的实现方法是由单个方法进行配置和调用

打包exposes

ContainerPlugin.js

 new ContainerPlugin({
  name: options.name,
  library,
  filename: options.filename,
  runtime: options.runtime,
  exposes: options.exposes
}).apply(compiler);

ContainerPlugin` 是一个webpack的插件主要的作用为“将exposes模块的内容封装打包成entry入口文件格式”,核心是实现容器的模块的加载与导出,从而在模块外侧进行一层的包装为了对模块进行传递与依赖分析\

ContainerPlugin主要作用将exposes的插件当成入口文件模块进行打包,形成单独可运行的文件,然后跟随主模块进行编译;

// 增加入口文件
      compilation.addEntry(
        compilation.options.context,
        dep,
        {
          name,
          filename,
          runtime,
          library
        },
        error => {
          if (error) return callback(error);
          callback();
        }
      );

最终expose的组件回被解析成一个入口文件,和main.js同级打印出来

打包remotes

加载远程模块内容

打包完成后,本质是将romote中的配置项目作为资源嵌入到打包模块中

  // 主要代码
  new ContainerReferencePlugin({
          remoteType,
          remotes: options.remotes
        }).apply(compiler);
        
 // 配置代码
   remotes: {
        component_app: 'component_app@http://localhost:3001/remoteEntry.js',
    },

ContainerReferencePlugin.js

ContainerReferencePlugin用于两个或多个不同Container的调用关系的判断,为了实现模块的通信与传递,通过调用反馈的机制实现模块间的传递;如romotejs的调用和host的关系

image.png Compiler 使用 NormalModuleFactory 模块生成各类模块。从入口点开始,此模块会分解每个请求,解析文件内容以查找进一步的请求,然后通过分解所有请求以及解析新的文件来爬取全部文件。在最后阶段,每个依赖项都会成为一个模块实例。在请求时会通过remote的方式进行加载对应资源;

理share模块

/webpack/lib/sharing 打包构建过程中的图 share模块的含义有两个 webpack/lib/sharing/SharePlugin.js

image.png

作为remote 模块中的share
// 序列化share module的内容
 [ 
    {
     'axios-share': {
       import: 'axios',
       shareKey: 'axios-share',
       shareScope: undefined,
       requiredVersion: '0.1.2',
       strictVersion: false,
       singleton: true,
       packageName: 'axios',
       eager: undefined
     }
    }
 ]
   // 消耗插件包
    new ConsumeSharedPlugin({
      shareScope: this._shareScope,
      consumes: this._consumes
    }).apply(compiler);

通过 ConsumeSharedPlugin 插件,序列化_consumes的内容,补充必须参数值,

image.png 在经过一系列处理后,最终会到模版层面,生成模块 运行打包:

federation-module/webpack5-ferderation-example/util_app/node_modules/webpack/lib/sharing/ConsumeSharedRuntimeModule.js

作为host模块 provides
[
   [
     'react',
     {
       shareKey: 'my-react',
       version: undefined,
       shareScope: 'default',
       eager: false
     }
     ]
 ]

针对消耗和提供,分别进行处理

    new ProvideSharedPlugin({
      shareScope: this._shareScope,
      provides: this._provides
    }).apply(compiler);
  1. 【normalModuleFactory.hooks.module】创建NormalModule实例后调用,查找路径、配置、版本

先校验已有的module中是否存在当前版本的provideShare,如不存在则进行创建provideModule,并将其添加至resourceResolveData

image.png

2.【compiler.hooks.finishMake】 创建完成 compilation

遍历compilationData并将其已经处理好的数据结构添加至 compilation中,最终调用_addEntryItem 添加一个单独模块入口文件(打包成一个新chunk)

Promise.all(
        Array.from(
          resolvedProvideMap,
          ([resource, { config, version }]) =>
            new Promise((resolve, reject) => {
             // 添加数据内容
              compilation.addInclude(
                compiler.context,
                // 创建ProvideSharedDependency依赖类
                new ProvideSharedDependency(
                  config.shareScope,
                  config.shareKey,
                  version || false,
                  resource,
                  config.eager
                ),
                {
                  name: undefined
                },
                err => {
                  if (err) return reject(err);
                  resolve();
                }
              );
            })
        )
      ).then(() => {});

走进module federation的微前端

微前端构建一个现代web所需要的技术策略、和方法。具备多个团队独立开发、独立部署的能力

微前端落地满足的三个要素

  1. 独立开发、独立交付;
  2. 无技术栈限制;
  3. 应用整合能力与通信能力。

微前端的三种模式

  • 容器模式 通过一个中心基座容器,对应用进行管理,串联应用之间的连接通信。具体实现上有 iframe 和 single-spa 框架等
  • 模块加载模式 去中心化🌟 没有中心容器,可以将任何一个微应用当作项目入口,整个模块的微应用和微应用模块之间串联,打破项目固定的模块加载模式,彻底释放项目的灵活性,俗称“去中心化”模式
  • 自组织模式 通过约定或规范进行互联互调,可以借助服务端技术或其他技术能力实现,只要符合微前端三要素则成立,比如 Nginx 路由分发,Nginx 在其中起到了中间环节分发和整合的作用,就类似一座桥梁,通往各个应用之间,最后再返回到浏览器这个目的地; 更多可以参考此文 :github.com/efoxTeam/em… 而模块联邦正是属于“模块加载模式”,任何一项项目都能够作为微应用的入口;无等级之分,

微前端demo

由百度团队Efox团队开发,使用module federation方案落地的微前端 更多可以了解:github.com/efoxTeam/em…

模块联邦 优缺点

优点

  • 使用
    • 解决了多个应用代码共享的问题,更加优雅的实现跨应用代码共享
    • 异步方式提供和使用共享模块,相对节约资源
    • 共享模块
      • 容器和应用程序都可以将共享模块和版本信息放在共享范围,共享范围中使用共享模块以及要求版本检查功能,
      • 对于重复的模块数据不进行加载
  • 学习成本
    • 基于webpack生态,学习成本、实施成本低
  • 工程化, 发布模式等随意
  • 配置简单易上手,官方也提供了基于各种框架的版本
  • 相关概念脉络清晰易懂

缺点

  • 使用
    • 无版本概念(打包构建静态资源不存在hash),影响面广,较为中心的应用修改了功能,发布功能一旦出错,影响面较大
    • 入口资源加载失败后,其暴露模块的相关功能将无法使用(不仅仅是“模块联邦”项目中)
    • 对于typescript的提示不友好

总结

本文通过对webpack5的module federation的相关配置,对打包后代码进行梳理,了解其运行时的相应状态,在从源码出发,简易的对module federation的打包流程进行阐述。 学习下来,收获还是很大的~🎉🎉

参考文档