微前端-Module federation的实现

4,527 阅读6分钟

在之前的文章中,我们介绍了微前端解决方案中的single-spa以及qiankun的实现。本篇文章,我们将介绍另外一种解决方案:webpack5中的Module federation

本片文章将从以下几个方面进行讲解。

  1. Module federation是什么
  2. Module federation该如何使用
  3. 从源码分析Module federation的实现原理
  4. Module federationqiankun以及single-spa的区别

为了方便书写,我们在下文同一用mf来代替Module federation

Module federation是什么

mf是webpack5的新插件,它的主要功能是我们可以将项目中的部分组件或全部组件暴露给外侧使用。我们也可以引用其他项目中暴露的组件,从而实现模块的复用。听起来mf好像与npm安装包的形式非常类似,都可以暴露组件给外部使用,我自己本身也可以使用其他安装包的组件。那他们的具体区别有哪些呢?

我们用个实例解释:

例如我们有一个项目a,要使用项目b中的某些功能。对于npm的形式,我们可以通过将b的项目打包之后,让项目a引用就行。假设这个时候突然项目b发现自己的写的某些功能有bug,那么也要项目a更新依赖包重新发布。如果是mf的形式,我们就只需要更新项目b即可。这是最大的区别。

Module federation该如何使用

mf是基于webpack5的,如果是项目中使用的构建是基于webpack4或其他版本的话,那么就需要对webpack进行升级改造。

在确保了webpack的版本之后,我们接下来所做的事情就是对webpack的打包配置进行配置。我们之前说了mf是webpack5的一个新插件,在此需要先介绍下这个插件中的配置项。

new ModuleFederationPlugin({
      name: "app_1",
      filename: 'remoteEntry.js'
      remotes: {
        app_two: "app_2",
        app_three: "app_3"
      },
      exposes: {
        AppContainer: "./src/App"
      },
      shared: ["react", "react-dom", "react-router-dom"]
    }),

name: 应用的名称。在其他应用查找的时候,会在这个name的作用域下去查找对应的组件。

remotes: 一个映射管理,将其他远程的名称映射成本地的别名。例如上面的我们将其他远程项目app_2映射成了本地的app_two

filename: 这些对外暴露的模块存放在哪个文件中。

exposes: 对外暴露的模块。只有对外暴露的相应的模块功能才能被使用。

shared: 制定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。

那么在对webpack进行了配置之后,我们在项目中应该具体怎么使用呢?

在项目中使用,分为两个步骤,第一个步骤首先是引用对应的模块打包后的脚本。例如app_2的remoteEntry.js。这个引用呢我们可以将对应的模块打包后的脚本部署到cdn上,然后在template.html中,将其引用。

image-20211004163312936

image-20211004163253975

引用完了之后,接下来就是该如何使用引用的脚本中的组件了。

const Button = React.lazy(() => import("app_three/Button"));

可以看到,app_three使我们在上文配置中对应用名为app_3的名字映射,我们在app_3的exposes中暴露了Button组件。所以引用的话,就直接使用import("应用别名/需要使用的组件");

从源码分析Module federation的实现原理

在上面我们讲解了该如何配置webpack以及如何使用mf。接下来就让我们一探究竟,这个到底是怎么实现的。

我们先来看下打包后的inde.html长什么样

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="http://localhost:3002/remoteEntry.js"></script>
    <script src="http://localhost:3003/remoteEntry.js"></script>
  </head>
  <body>
    <div id="root"></div>
  <script src="http://localhost:3001/remoteEntry.js"></script><script src="http://localhost:3001/main.js"></script></body>
</html>

可以看到我们是先加载了需要引用的应用的组件(<script src="http://localhost:3002/remoteEntry.js"></script>),那么我们就看看这个js文件到底输出了什么。我们可以看到首先他在在全局作用域上定义了一个app_03,然后将一个自执行函数的值,赋值给了app_03。在这自执行函数内部,创建了一个_webpack_modules__的变量,内部有一个属性?eb9c,为了后面方便说明,我们日后将?eb9c叫做app_03的模块标识。app_03的模块标识的值又是一个自执行函数,在这个自执行函数中定义了一个moduleMap变量,这个变量内存储的就是对外暴露的模块。接下来呢,是两个方法,一个是get方法,判断外侧想要的模块在moduleMap中是否存在,如果不存在则抛出错误。最后呢,另外一个就是对shared的合并操作。最后,将这两个方法挂载到exports属性上暴露出去。

// 摘取部分
var app_03;app_03 =
(() => { // webpackBootstrap
  var __webpack_modules__ = ({
    "?eb9c": ((__unused_webpack_module, exports, __webpack_require__) => {
      var moduleMap = {
        "Button": () => {
          return Promise.all([__webpack_require__.e("vendors-node_modules_styled-components_dist_styled-components_browser_esm_js"), __webpack_require__.e("src_Button_jsx-")]).then(() => () => __webpack_require__(/*! ./src/Button */ "./src/Button.jsx"));
        }
      };
  var get = (module) => {
    return (
      __webpack_require__.o(moduleMap, module)
        ? moduleMap[module]()
        : Promise.resolve().then(() => {
          throw new Error("Module " + module + " does not exist in container.");
        })
    );
  };
  var override = (override) => {
    Object.assign(__webpack_require__.O, override);
  }
​
// This exports getters to disallow modifications
__webpack_require__.d(exports, {
  get: () => get,
  override: () => override
});
​
/***/ })
})()

在加载了应用的组件对应脚本之后,我们在项目中的使用方法是:

// ...去除不重要代码
const Button = React.lazy(() => import("app_03/Button"));
// ... 去除不重要代码

接着我们来看上面这个代码打包后的样子是什么样的。

image-20211004170258329

我们可以看出,上面那句代码被翻译成了

const Button = react__WEBPACK_IMPORTED_MODULE_2___default().lazy(() => __webpack_require__.e(/*! import() */ "-_c6ab").then(__webpack_require__.t.bind(__webpack_require__, /*! app_03/Button */ "?c6ab", 7)));
// 为了方便理解,我将main.js中的相关参数定义放在下方
// __webpack_require__.e
__webpack_require__.f = {
 
};__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__.e这个方法,并且传了一个表示_c6ab,然后在调用.then方法。在__webpack_require__.e这个方法中,我们看出,他首先获取__webpack_require__.f上的所有key,之后再执行对应的函数,返回一个promise的数组。在__webpack_require__.f上有一个专门获取远程相关数据的方法,如下:

*/  (() => {
/******/    var chunkMapping = {
/******/      "-_95f2": [
/******/        "?95f2"
/******/      ],
/******/      "-_6133": [
/******/        "?6133"
/******/      ],
/******/      "-_c6ab": [
/******/        "?c6ab"
/******/      ]
/******/    };
/******/    var idToExternalAndNameMapping = {
/******/      "?95f2": [
/******/        "?5d41",
/******/        "Dialog"
/******/      ],
/******/      "?6133": [
/******/        "?5d41",
/******/        "Tabs"
/******/      ],
/******/      "?c6ab": [
/******/        "?702f",
/******/        "Button"
/******/      ]
/******/    };
/******/    __webpack_require__.f.remotes = (chunkId, promises) => {
/******/      if(__webpack_require__.o(chunkMapping, chunkId)) {
/******/        chunkMapping[chunkId].forEach((id) => {
/******/          if(__webpack_modules__[id]) return;
/******/          var data = idToExternalAndNameMapping[id];
/******/          promises.push(Promise.resolve(__webpack_require__(data[0]).get(data[1])).then((factory) => {
/******/            __webpack_modules__[id] = (module) => {
/******/              module.exports = factory();
/******/            }
/******/          }))
/******/        });
/******/      }
/******/    }
/******/  })();

看到这里基本就恍然大悟了,当key为reomte的时候,他调用__webpack_require__.f[key](chunkId, promises)的时候,也就是相当于在调用__webpack_require__.f.remotes(chunkId, promises),在这个方法里如果这个chunkId找的到的话,那么就会去idToExternalAndNameMapping里面去匹配,匹配之后,会返回一个数组,数组的第一项对应的是当前当前上下文中的container-reference/app_03的key,第二项则为需要具体加载的东西。(container-reference/app_03是标识,标识容器引用的是全局变量下的app_03)。

做一个总结就是:

  1. 加载其他应用的组件通过mf打包后暴露出来的文件remoteEntry.js
  2. 执行remoteEntry.js,在全局作用域下挂载一个名为在mf中定义的name的属性,这个属性暴露了get和override这两个方法
  3. 在组件中引用的时候,会通过__webpack_require__.e去进行引用。
  4. __webpack_require__.e中调用__webpack_require__.f中的对应的方法,从而得到相应的组件。

Module federationqiankun以及single-spa的区别

共同点:

都能实现微前端。

不同点:

  1. qiankunsingle-spa是基于应用的,而mf是基于组件的。
  2. mf对于无限套娃模式支持比较友好,
  3. mf对于老项目不太友好,需要升级对应的webpack,不能直接使用.html文件。
  4. single-spa一样,不支持js沙盒环境,需要自己进行实现。
  5. 第一次需要将引用的依赖前置,会导致加载时间变成的问题。