模块联邦
一、模块联邦概述
- Module Federation 即为模块联邦,是Webpack5中新增的功能。可以实现跨应用共享模块,如图所示:
2.准备实现以下应用
3.应用的结构如图所示:
4.切换到products目录下,安装以下依赖:
npm i faker@5.5.2 -S
npm i html-webpack-plugin -S
npm i webpack -S
npm i webpack-cli -S
npm i webpack-dev-server -S
5.更改products/package.json启动命令
{
"scripts": {
"start": "webpack serve"
},
}
6.配置webpack文件products/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
mode: "development",
devServer: {
port: 8081
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
7.在入口html中加入盒子元素products/public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dev-products</title>
</head>
<body>
<div id="dev-products"></div>
</body>
</html>
8.在入口js文件中加入产品名称列表products/src/index.js
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
9.启动项目,npm start
,访问 http://localhost:8081/,如图所示:
10.复制products,创建 container 和 cart应用
二、通过配置模块联邦实现在容器应用中加载产品列表微应用
1.在产品列表微应用中将自身作为模块进行导出products/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin")
// 导入模块联邦插件
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
module.exports = {
mode: "development",
devServer: {
port: 8081
},
plugins: [
// 将 products 自身当做模块暴露出去
new ModuleFederationPlugin({
// 模块文件名称,其他应用引入当前模块时需要加载的文件的名字
filename: "remoteEntry.js",
// 模块名称,具有唯一性
name: "products",
// 当前模块具体导出的内容
exposes: {
"./Index": "./src/index"
}
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
2.在容器应用中导入产品列表微应用,在之前复制的container基础上修改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”和被导入模块的name属性值对应
// 属性 “products” 是映射名,是在当前应用中导入该模块时使用的名字
products: "products@http://localhost:8081/remoteEntry.js"
}
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
3.使用导入的模块container/src/index.js
import("products/Index").then(products => console.log(products))
4.访问http://localhost:8080/,如图所示,可以在容器中看到微应用的内容
5.步骤3中的写法,意味着所有使用导入模块内容的方法,必须写在import的回调中,不便于维护,在src文件新建bootstrap.js文件
// container/src/bootstrap.js
import("products/Index")
那么container/src/index.js就变成如下写法
import("./bootstrap")
三、加载cart微应用
1.还是复制之前的products文件,修改cart/webpack.config.js文件
const HtmlWebpackPlugin = require("html-webpack-plugin")
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
module.exports = {
mode: "development",
devServer: {
port: 8082
},
plugins: [
new ModuleFederationPlugin({
name: "cart",
filename: "remoteEntry.js",
exposes: {
"./Index": "./src/index"
}
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
2.修改cart/src/index.js文件
import faker from "faker"
document.querySelector("#dev-cart").innerHTML = `你已经加购了${faker.random.number()}件商品`
3.修改cart/public/index.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>购物车</title>
</head>
<body>
<div id="dev-cart"></div>
</body>
</html>
4.运行cart项目,npm start
访问http://localhost:8082/,如下所示:
5.配置容器应用container/webpack.config.js
remotes: {
// 新增
cart: "cart@http://localhost:8082/remoteEntry.js"
}
6.增加cart显示的容器container/public/index.html
<div id="dev-cart"></div>
7.引用cart应用container/src/bootstrap.js
import("products/Index")
import("cart/Index")
8.重启container项目,访问http://localhost:8080/,出现如下情况:
这种诡异的情况是因为之前直接复制package.json文件,name属性没有改成cart,所以我们需要将container和cart中的package.json的name属性改成对应的应用名称。
package.json 中最重要的属性是 name
和 version
两个属性,这两个属性是必须要有的,否则模块就无法被安装,这两个属性一起形成了一个 npm 模块的唯一标识。
name是 package(包)的名称。名称的第一部分(如@scope/是可选的,用作名称空间)。当我们的包发布到 NPM 网站,其他人才能通过搜索name来安装使用
修改后重新启动,访问http://localhost:8080/ 如图所示:
9.在控制台可以看到faker被加载了两次,如果应用个数过多,性能就会受到影响
这时可以通过配置webpack文件实现模块共享
// cart/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin")
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
module.exports = {
mode: "development",
devServer: {
port: 8082
},
plugins: [
new ModuleFederationPlugin({
name: "cart",
filename: "remoteEntry.js",
exposes: {
"./Index": "./src/index"
},
shared: ["faker"]
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
// products/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin")
// 导入模块联邦插件
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
module.exports = {
mode: "development",
devServer: {
port: 8081
},
plugins: [
// 将 products 自身当做模块暴露出去
new ModuleFederationPlugin({
// 模块文件名称,其他应用引入当前模块时需要加载的文件的名字
filename: "remoteEntry.js",
// 模块名称,具有唯一性
name: "products",
// 当前模块具体导出的内容
exposes: {
"./Index": "./src/index"
},
shared: ["faker"]
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
注意:共享模块需要异步加载,在 Products 和 Cart 中需要添加 bootstrap.js
// cart/src/bootstrap.js
import faker from "faker"
document.querySelector("#dev-cart").innerHTML = `你已经加购了${faker.random.number()}件商品`
// cart/src/index.js
import("./bootstrap")
// products/src/bootstrap.js
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
// products/src/index.js
import("./bootstrap")
重新启动3个应用,访问http://localhost:8080/,效果如图所示:
可以看到只加载了一次faker
10.如果应用的共享版本冲突,会发生什么呢?将cart的版本改为^4.1.0,重新运行,如图所示
可以看到之前的设置失效了,现在再来改一下cart和products的webpack.config.js文件
// cart/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin")
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
module.exports = {
mode: "development",
devServer: {
port: 8082
},
plugins: [
new ModuleFederationPlugin({
name: "cart",
filename: "remoteEntry.js",
exposes: {
"./Index": "./src/index"
},
shared: {
faker: {
singleton: true
}
}
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
// products/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin")
// 导入模块联邦插件
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
module.exports = {
mode: "development",
devServer: {
port: 8081
},
plugins: [
// 将 products 自身当做模块暴露出去
new ModuleFederationPlugin({
// 模块文件名称,其他应用引入当前模块时需要加载的文件的名字
filename: "remoteEntry.js",
// 模块名称,具有唯一性
name: "products",
// 当前模块具体导出的内容
exposes: {
"./Index": "./src/index"
},
shared: {
faker: {
singleton: true
}
}
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
重新运行3个应用,可以看到又变成只加载一个faker了
这个配置项的意思是使用版本高的共享模块,同时我们会在控制台看到一个来自cart的告警
11..在容器应用导入微应用后,应该有权限决定微应用的挂载位置,而不是微应用在代码运行时直接进行挂载。所以每个微应用都应该导出一个挂载方法供容器应用调用。
11-1.修改cart
// cart/src/bootstrap.js
import faker from "faker"
function mount(el) {
el.innerHTML = `你已经加购了${faker.random.number()}件商品`
}
export { mount }
// cart/webpack.config.js
// 修改对外暴露文件
const HtmlWebpackPlugin = require("html-webpack-plugin")
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
module.exports = {
mode: "development",
devServer: {
port: 8082
},
plugins: [
new ModuleFederationPlugin({
name: "cart",
filename: "remoteEntry.js",
exposes: {
"./Index": "./src/bootstrap"
},
shared: {
faker: {
singleton: true
}
}
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
11-2.修改products
// products/src/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
}
export { mount }
// products/webpack.config.js
// 修改对外暴露文件
const HtmlWebpackPlugin = require("html-webpack-plugin")
// 导入模块联邦插件
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
module.exports = {
mode: "development",
devServer: {
port: 8081
},
plugins: [
// 将 products 自身当做模块暴露出去
new ModuleFederationPlugin({
// 模块文件名称,其他应用引入当前模块时需要加载的文件的名字
filename: "remoteEntry.js",
// 模块名称,具有唯一性
name: "products",
// 当前模块具体导出的内容
exposes: {
"./Index": "./src/bootstrap"
},
shared: {
faker: {
singleton: true
}
}
}),
new HtmlWebpackPlugin({
template: "./public/index.html"
})
]
}
11-3.修改容器container
// container/src/bootstrap.js
import { mount as mountCart } from "cart/Index"
import { mount as mountProducts } from "products/Index"
mountCart(document.querySelector("#dev-cart"))
mountProducts(document.querySelector("#dev-products"))
重新运行所有应用,访问http://localhost:8080/,如图所示
12.现在容器应用访问没有问题,但是单独访问cart和products就不显示,是因为刚才改造mount方法时,没有处理自己应用的挂载
// cart/src/bootstrap.js
import faker from "faker"
function mount(el) {
el.innerHTML = `你已经加购了${faker.random.number()}件商品`
}
if (process.env.NODE_ENV === 'development') {
const el = document.querySelector("#dev-cart")
// 这里判读el,是因为访问容器项目时,也会进入这个分支,当容器项目中的挂载点名称不是dev-cart时,会找不到el
if(el) mount(el)
}
export { mount }
重启cart,访问http://localhost:8082/,如图所示:
// products/src/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
}
if (process.env.NODE_ENV === 'development') {
const el = document.querySelector("#dev-products")
// 这里判读el,是因为访问容器项目时,也会进入这个分支,当容器项目中的挂载点名称不是dev-products时,会找不到el
if(el) mount(el)
}
export { mount }
重启products,访问http://localhost:8081/,如图所示