Module Federation 即为模块联邦,是 Webpack 5 中新增的一项功能,可以实现跨应用共享模块。
需求:通过模块联邦在容器应用中加载微应用。
目录结构:
- 应用初始化
在入口 JavaScript 文件中加入产品列表
import faker from "faker"
let products = ""
for (let i = 1; i <= 5; i++) {
products += `<div>${faker.commerce.productName()}</div>` }
document.querySelector("#dev-products").innerHTML = products
在入口 html 文件中加入盒子
<div id="dev-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
- 联邦模块配置
通过配置模块联邦实现在容器应用中加载产品列表微应用。
1.在产品列表微应用中将自身作为模块进行导出
// 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"
应用webpack打包分析和容器应用文件加载顺序分析
-
Products 应用打包分析
-
Container 应用打包分析
- 文件加载顺序分析
在Container应用中加载Cart应用
// cart/webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
new ModuleFederationPlugin({
name: "cart",
filename: "remoteEntry.js",
exposes: { "./index": "./src/index" }
})
// container/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin")
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
module.exports = {
mode: "development",
devServer: {
port: 8080
},
plugins: [
// 配置联邦模块插件
new ModuleFederationPlugin({
name: "container",
remotes: {
products: "products@http://localhost:8081/remoteEntry.js",
+ cart: "cart@http://localhost:8082/remoteEntry.js"
}
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
// container/bootstrap.js
import "cart/index"
<!-- container/index.html -->
<div id="dev-cart"></div>
注意:cart/index.html 和 products/index.html 仅仅是在开发阶段中各自团队使用的文件,而container/index.html 是在开发阶段和生产阶段都要使用的文件。
模块联邦 - 实现共享模块
在 Products 和 Cart 中都需要 Faker,当 Container 加载了这两个模块后,Faker 被加载了两次。
// 分别在 Products 和 Cart 的 webpack 配置文件中的模块联邦插件中添加以下代码
{ shared: ["faker"] }
// 重新启动 Container、Products、Cart
例如Container/webpack.config.js:
plugins: [
new ModuleFederationPlugin({
name: "cart",
filename: "remoteEntry.js",
exposes: {
"./Index": "./src/bootstrap"
},
shared: ["faker"]
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
注意:共享模块需要异步加载,在 Products 和 Cart 中需要添加 bootstrap.js
解决共享模块版本冲突的问题
Cart 中如果使用 4.1.0 版本的 faker,Products 中使用 5.2.0 版本的 faker,通过查看网络控制面板可以发现 faker 又会被加载了两次,模块共享失败。
解决办法是分别在 Products 和 Cart 中的 webpack 配置中加入如下代码:
shared: {
faker: { singleton: true }
}
例如:cart/webpack.config.js
plugins: [
new ModuleFederationPlugin({
name: "cart",
filename: "remoteEntry.js",
exposes: {
"./Index": "./src/bootstrap"
},
+ shared: {
faker: {
singleton: true
}
}
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
注意:但同时会在原本使用低版本的共享模块应用的控制台中给予警告提示。
容器应用通过mount方法挂载微应用
在容器应用导入微应用后,应该有权限决定微应用的挂载位置,而不是微应用在代码运行时直接进行挂载。所以每个微应用都应该导出一个挂载方法供容器应用调用。
// Products/bootstrap.js
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 }
// Products/webpack.config.js
exposes: {
// ./src/index => ./src/bootstrap 为什么 ?
// mount 方法是在 bootstrap.js 文件中导出的, 所以此处要导出 bootstrap // 此处的导出是给容器应用使用的, 和当前应用的执行没有关系, 当前应用在执行时依然先执行 index
"./index": "./src/bootstrap"
}
// Container/bootstrap.js
import { mount as mountProducts } from "products/index"
mountProducts(document.querySelector("#my-products"))