webpack技术-微后端和 webpack 5 模块联合

目前微前端的落地方案可以分为:自组织模式、基础模式、模块加载模式。

与基础模式相比webpack技术,模块加载模式没有中心容器(去中心化模式),这意味着任何微应用都可以作为模块入口,整个项目的微应用串联起来。 具体代表库是qiankun vs EMP。

实现模块加载方式需要依赖webpack5的Module Federation功能。

什么是模块联盟?

多个独立创作可以组成一个应用程序。 这些独立构建之间不应该存在依赖关系,因此它们可以独立开发和部署,通常称为微后端,但它的作用仅此而已! 通俗地说,Module Federation提供了在当前应用程序中加载其他应用程序的能力。

因此,如果当前模块想要加载其他模块,就必须有一个import动作。 同样,如果它想让其他模块使用它,它必须有一个导入动作。

因此引入了webapck配置的两个概念:

Exposure:导出应用程序,被其他应用程序导出

远程:引入其他应用程序

这和base模式完全不同,像single-spa和qiankun都需要一个base(中央容器)来加载其他子应用。 但Module Federation的任何模块都可以引用其他应用程序,也可以被其他应用程序导入和使用,所以不存在容器中心的概念。

模块联合配置分析

使用Module Federation需要引入外部插件ModuleFederationPlugin。 暴露参数指定需要导入哪个模块,并配置要在远程导出的应用程序。

下面的示例代码,以vue3为例,是消费者主机应用在其主页加载远程home应用的按钮和内容组件:

消费者应用程序 webpack 配置

const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin
module.exports = {
    plugins: [
         new ModuleFederationPlugin({
          // 唯一ID,当前微应用名称
          name: "comsumer",
          filename: "remoteEntry.js",
          // 导入模块
          remotes: {
          // 导入后给模块起个别名:“微应用名称@地址/导出的文件名”
            home: "home@http://localhost:3002/remoteEntry.js",
          },
          exposes: {},
          // 与其他应用之间可以共享的第三方依赖,使你的代码中不用重复加载同一份依赖
          shared: ['vue']
        })
    ]
}

家庭应用程序 webpack 配置

const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin
module.exports = {
    plugins: [
      new ModuleFederationPlugin({
          // 唯一ID,当前微应用名称
          name: "home",
          // 对外提供的打包后的文件名(引入时使用)
          filename: "remoteEntry.js",
          // 暴露的应用内具体模块
          exposes: {
          // 名称: 代码路径
            "./Content": "./src/components/Content",
            "./Button": "./src/components/Button",
          },
          // 与其他应用之间可以共享的第三方依赖,使你的代码中不用重复加载同一份依赖
          shared: ['vue']
        })
    ],
     devServer: {
        port: 3002,
      },
}

在 HOST 主机应用程序中导出远程应用程序内容

引用微应用返回一个Promise,它最终会返回一个“模块对象”结果,default是默认导入内容的结果。 消费者应用加载home应用的代码如下:

import { createApp, defineAsyncComponent } from "vue";
import Layout from "./Layout.vue";
// 加载远程Content组件
const Content = defineAsyncComponent(() => import("home/Content"));
// 加载远程Buttom组件
const Button = defineAsyncComponent(() => import("home/Button"));
const app = createApp(Layout);
app.component("content-element", Content);
app.component("button-element", Button);
app.mount("#app");

webpack教程_webpack安装教程_webpack技术

Module Federation的重构代码分析

webpack打包时webpack的mf配置会进行哪些操作? 打包后的结果代码如何加载远程模块? 如何导入自己的模块并提供它们以供其他应用程序导出?

首先看consumer应用导出到home应用的代码webpack技术,截取部分代码。 消费者应用程序需要导入家庭应用程序的两个远程组件。 它会首先加载148模块,即remoteEntry.js:

app.component("content-element", Content);
app.component("button-element", Button);

main.js

var chunkMapping = {
  "186": [
    186
  ],
  "190": [
    190
  ]
};
var idToExternalAndNameMapping = {
  "186": [
    "default",
    "./Content",
    148
  ],
  "190": [
    "default",
    "./Button",
    148
  ]
};
__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);
            // 首先加载148模块,即remoteEntry.js
            handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
        });
    }
};
 148: ((module, __unused_webpack_exports, __webpack_require__) => {
    "use strict";
    var __webpack_error__ = new Error();
    module.exports = new Promise((resolve, reject) => {
        if(typeof home !== "undefined") return resolve();
        __webpack_require__.l("http://localhost:3002/remoteEntry.js", (event) => {}, "home");
    }).then(() => (home));
 })

查看home模块的remoteEntry.js代码,提取部分代码:

var moduleMap = {
	"./Content": () => {
		return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Content_vue-_56df0")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Content */ "./src/components/Content.vue")))));
	},
	"./Button": () => {
		return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Button_js-_e56a0")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Button */ "./src/components/Button.js")))));
	}
};

我们来看看 moduleMap。 在返回对应组件之前,通过__webpack_require__.e加载其对应的依赖。 虽然我们看到 __webpack_require__.e 并行执行 __webpack_require__.f 中的方法:

/* webpack/runtime/ensure chunk */
(() => {
	__webpack_require__.f = {};
	// This file contains only the entry chunk.
	// The chunk loading function for additional chunks
	__webpack_require__.e = (chunkId) => {	
        // __webpack_require__.f中的所有方法并行执行
            return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
                    __webpack_require__.f[key](chunkId, promises);
                    return promises;
            }, []));
	};
})();

我们看一下__webpack_require__.f上都有哪些函数,最后发现有三个:remotes、consumes、j。

webpack教程_webpack安装教程_webpack技术

__webpack_require__.o 也指 Object.prototype.hasOwnProperty。

// no consumes in initial chunks
var chunkMapping = {
    webpack_sharing_consume_default_vue_vue: [
        "webpack/sharing/consume/default/vue/vue",
    ],
};
__webpack_require__.f.consumes = (chunkId, promises) => {
    if (__webpack_require__.o(chunkMapping, chunkId)) {
        chunkMapping[chunkId].forEach((id) => {
            if (__webpack_require__.o(installedModules, id))
                return promises.push(installedModules[id]);
            try {
                var promise = moduleToHandlerMapping[id]();
                if (promise.then) {
                    promises.push(
                        (installedModules[id] = promise.then(onFactory).catch(onError))
                    );
                } else onFactory(promise);
            } catch (e) {
                onError(e);
            }
        });
    }
};

var installedChunks = {
    home: 0,
};
__webpack_require__.f.j = (chunkId, promises) => {
    // JSONP chunk 加载
    var installedChunkData = __webpack_require__.o(installedChunks, chunkId)
            ? installedChunks[chunkId]
            : undefined;
    // 0 表示已经加载过了
    if (installedChunkData !== 0) {
        // Promise 表示正在加载
        if (installedChunkData) {
                promises.push(installedChunkData[2]);
        } else {
            // 加载chunk,但是排除了webpack_sharing_consume_default_vue_vue这个共享的包
            if ("webpack_sharing_consume_default_vue_vue" != chunkId) {
                __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
            } else installedChunks[chunkId] = 0;
        }
    }
};

综上所述:

首先加载main.js并将其注入到html中的main.js中。 如果需要动态加载远程Content和Button组件,需要先加载remoteEntry.js。 在remoteEntry.js中,通过__webpack_require__.e加载moduleMap中的创建遍历,执行完__webpack_require__.f中的函数后,需要先加载并构建依赖。 加载分片依赖项后,加载内容和按钮组件。 我对Federarion模块的理解

目前官方文档给出的几个用例,我想肯定会成为未来后端开发的大趋势。 模块联合可以应用于微后端,但不限于此。

敢于想象,当我们可以

光是想想就让我感到兴奋。

Module Federation更崇高地解决了公共依赖加载和共享的问题,这也是基础模型难以处理的地方。

Module Federation也有很多demo用例,比如

另外,EMP微后端解决方案已经在github上开源,开箱即可使用。

webpack技术_webpack安装教程_webpack教程

但仍有很多问题需要抱怨

尝试了模块联邦的功能后,各种报错,让人崩溃……只好通过各种方式寻找解决办法。 以下是我遇到的一些问题:

vue3+ vue-cli + webpack ^5.61.0

Uncaught (in promise) ScriptExternalLoadError: Loading script failed.
(missing: http://localhost:8000/remote.js)
while loading "./HelloWorld" from webpack/container/reference/app1

从加载截图来看,远程remote.js已加载,但remote.js发送的src_components_HelloWorld_vue.js未加载。 但是没有vue cli,直接自己写配置是没有问题的。 估计还是vue cli的支持问题。

解决办法是:去掉发包的配置,但这也是临时解决办法,肯定不好,但是还是可以让你体验一下功能的。

chainWebpack: (config) => {
        config.optimization.delete("splitChunks");
},

共享公共图书馆(共享)

比如我们的共享库vue如果没有异步加载入口文件的内容,就会报错如下:

webpack安装教程_webpack技术_webpack教程

Uncaught Error: Shared module is not available for eager consumption: webpack/sharing/consume/default/vue/vue

解决办法是:添加一个新的bootstrap.js文件,其中包含原来入口js文件的内容,然后异步加载入口文件中的bootstrap.js文件,这样代码就可以正常运行了。

具体代码如下:

入口文件:main.js

// 必须是异步加载
import("./bootstrap"); 

bootstrap.js:内容为原入口文件内容

import { createApp, defineAsyncComponent } from "vue";
import Layout from "./Layout.vue";
const Content = defineAsyncComponent(() => import("home/Content"));
const Button = defineAsyncComponent(() => import("home/Button"));
const app = createApp(Layout);
app.component("content-element", Content);
app.component("button-element", Button);
app.mount("#app");

这是因为远程remoteEntry.js文件需要在src_bootstrap_js.js中的内容之前加载并执行。 如果不异步加载bootstrap.js,而是直接执行原来的入口代码,但是原来的入口代码依赖于远程js的代码,而远程js的代码还没有加载,就会报错。 直观上,从上图中js文件的大小可以看出,部分代码从原来的main.js移到了src_bootstrap_js.js,并且remoteEntry.js先于src_bootstrap_js.js执行。