目前微前端的落地方案可以分为:自组织模式、基础模式、模块加载模式。
与基础模式相比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");
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_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上开源,开箱即可使用。
但仍有很多问题需要抱怨
尝试了模块联邦的功能后,各种报错,让人崩溃……只好通过各种方式寻找解决办法。 以下是我遇到的一些问题:
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如果没有异步加载入口文件的内容,就会报错如下:
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执行。
发表评论