Module Federation 模块联邦
通过 模块联邦 也可以实现微前端架构
Module Federation 模块联邦 概述
Module Federation 即为模块联邦,是 Webpack 5 中新增的一项功能,可以实现跨应用共享模块。
我们有两个应用:A应用和B应用。
A应用中提供了一个方法:sayHelloFromA
B应用中提供了一个方法:sayHelloFromB
我们想要在A应用中调用B应用提供的方法,B应用调用A应用提供的方法【跨应用调用方法】
我们可以把一个应用当做一个模块,这样我们就可以在一个应用中加载另一个应用了,只要我们可以在一个应用中加载另一个应用我们就可以实现微前端架构
快速开始-demo
需求:通过模块联邦在容器应用中加载微应用
1. 创建应用结构
按照以下目录结构构建好三个微应用的应用结构,package.json
和 package-lock.json
文件初始体验可以直接 copy 完成demo 中的文件
html 中初始化好各自的内容
安装各自的依赖
products
├── package-lock.json
├── package.json
├── public // 静态资源文件
│ └── index.html
├── src // 源码
│ └── index.js
└── webpack.config.js
2. 应用初始化
-
在入口 JavaScript 文件中加入产品列表
// faker 可以用来随机生成数据
import faker from 'faker'
let products = '';
for (let i = 1; i <= 5; i++) {
products += `<div>${faker.commerce.productName()}</div>`
}
document.querySelector("#products").innerHTML = products -
在入口 html 文件中加入盒子
<div id="products"></div>
-
webpack 配置
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
mode: "development",
devServer: {
port: 8081
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
} -
添加应用启动命令
"scripts": {
"start": "webpack serve"
}, -
通过 copy 的方式创建 container 和 cart
3. Module Federation
通过配置模块联邦实现在容器应用中加载产品列表微应用。
-
在产品列表微应用中将自身作为模块进行导出
// webpack.config.js
// 导入模块联邦插件
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
// 将 products 自身当做模块暴露出去
new ModuleFederationPlugin({
// 模块文件名称, 其他应用引入当前模块时需要加载的文件的名字
filename: "remoteEntry.js",
// 模块名称, 具有唯一性, 相当于 single-spa 中的组织名称
name: "products",
// 当前模块具体导出的内容
exposes: { "./index": "./src/index" },
});
// 在容器应用中要如何引入产品列表应用模块?
// 1. 在容器应用中加载产品列表应用的模块文件
// 2. 在容器应用中通过 import 关键字从模块文件中导入产品列表应用模块 -
在容器应用的中导入产品列表微应用
// webpack.config.js
// 导入模块联邦插件
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
new ModuleFederationPlugin({
name: "container",
// 配置导入模块映射
remotes: {
// 字符串 "products" 和被导入模块的 name 属性值对应
// 属性 products 是映射别名, 是在当前应用中导入该模块时使用的名字
products: "products@http://localhost:8081/remoteEntry.js",
},
});// src/index.js
// 因为是从另一个应用中加载模块, 要发送请求所以使用异步加载方式
import("products/index").then(products => console.log(products))通过上面这种方式加载在写法上多了一层回调函数, 不是很方便, 所以一般都会在 src 文件夹中建立bootstrap.js,在形式上将写法变为同步
// src/index.js
import('./bootstrap.js')
// src/bootstrap.js
import "products/index" -
实现 Cart 微应用加载 注意:cart/index.html 和 products/index.html 仅仅是 在开发阶段中各自团队使用的文件,而 container/index.html 是在开发阶段和生产阶段都要使用的文件。
共享模块
实现模块共享
在 Products 和 Cart 中都需要 Faker,当 Container 加载了这两个模块后,Faker 被加载了两次。
// 分别在 Products 和 Cart 的 webpack 配置文件中的模块联邦插件中添加以下代码
// 如果遇到版本不一致的问题,强制使用高版本
{
shared: ["faker"]
}
// 重新启动 Container、Products、Cart
注意:共享模块需要异步加载,在 Products 和 Cart 中需要添加 bootstrap.js
共享模块版本冲突解决
Cart 中如果使用 4.1.0 版本的 faker,Products 中使用 5.2.0 版本的 faker,通过查看网络控制面板可以发现 faker 又会被加载了两次,模块共享失败。 解决办法是分别在 Products 和 Cart 中的 webpack 配置中加入如下代码
shared: {
faker: {
singleton: true
}
}
但同时会在原本使用低版本的共享模块应用的控制台中给予警告提示。
开放子应用挂载接口
在容器应用导入微应用后,应该有权限决定微应用的挂载位置,而不是微应用在代码运行时直接进行挂 载。所以每个微应用都应该导出一个挂载方法供容器应用调用。
// faker 可以用来随机生成数据
import faker from "faker";
function mount(el) {
let products = "";
for (let i = 1; i <= 5; i++) {
products += `<div>${faker.commerce.productName()}</div>`;
}
el.innerHTML = products;
}
// 此处代码是 products 应用在本地开发环境下执行的
if (process.env.NODE_ENV === "development") {
const el = document.querySelector("#dev-products");
// 当容器应用在本地开发环境下执行时也可以进入到以上这个判断, 容器应用在执行当前代码时肯定是获 取不到 dev-products 元素的, 所以此处还需要对 el 进行判断.
if (el) mount(el);
}
export { mount };
exposes: {
// ./src/index => ./src/bootstrap 为什么 ?
// mount 方法是在 bootstrap.js 文件中导出的, 所以此处要导出 bootstrap
// 此处的导出是给容器应用使用的, 和当前应用的执行没有关系, 当前应用在执行时依然先执行 index
"./Index": "./src/bootstrap",
},
import { mount as mountProducts } from "products/Index";
mountProducts(document.querySelector("dev-products"));