Module Federation 即为模块联邦,是 Webpack 5 中新增的一项功能,可以实现跨应用共享模块。
项目创建
假设我们有产品、购物车两个项目,当然还有个用于承载的容器项目container
先创建产品products
项目,需要安装如下npm
npm i -D faker@5.2.0 html-webpack-plugin@4.5.1 webpack@5.19.0 webpack-cli@4.4.0 webpack-dev-server@3.11.2
products目录结构
products
├── package-lock.json
├── package.json
├── public
│ └── index.html
├── src
│ └── index.js
└──
webpack.config.js
修改scripts
命令
"start": "webpack serve"
配置webpack
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
devServer: {
port: 8081,
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};
index.js
mock产品列表
// 使用faker.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;
index.html
创建空的div
<div id="dev-products"></div>
同样copy两个并列的项目cart container,修改cart端口8082
,<div id="dev-cart"></div>
联邦实现
现在我们有了容器和子项目,怎么才能让这三者结合到一起呢?答案是使用webpack5中的ModuleFederationPlugin
插件
在products微应用中将自身作为模块进行导出
// 导入模块联邦插件
const ModuleFederationPlugin =
require("webpack/lib/container/ModuleFederationPlugin")
// 将 products 自身当做模块暴露出去
new ModuleFederationPlugin({
// 模块文件名称, 其他应用引入当前模块时需要加载的文件的名字
filename: "remoteEntry.js",
// 模块名称, 具有唯一性, 相当于 single-spa 中的组织名称
name: "products",
// 当前模块具体导出的内容
exposes: {
"./index": "./src/index"
}
})
在容器应用中要如何引入产品列表应用模块?
- 在容器应用中加载产品列表应用的模块文件
- 在容器应用中通过 import 关键字从模块文件中导入产品列表应用模块
在container容器应用的中导入产品列表微应用
// 导入模块联邦插件
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"
// public/index.html
<div id="dev-products"></div>
使用同样的方法,导入导出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
remotes: {
cart: "cart@http://localhost:8082/remoteEntry.js"
}
// src/bootstrap.js
import "cart/index"
// public/index.html
<div id="dev-cart"></div>
模块共享
在 Products 和 Cart 中都需要 Faker
,当 Container 加载了这两个模块后,Faker
被加载了两次😮。
// 分别在 Products 和 Cart 的 webpack 配置文件中的模块联邦插件中添加以下代码
{
shared: ["faker"]
}
// 重新启动 Container、Products、Cart
还有个问题,如果faker版本冲突怎么办,faker还是会加载两次,这样解决😉
shared: {
faker: {
singleton: true
}
}
开放子应用挂载接口
在容器应用导入微应用后,应该有权限决定微应用的挂载位置,而不是微应用在代码运行时直接进行挂 载。所以每个微应用都应该导出一个挂载方法供容器应用调用。
// 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"))
至此,你已经了解了模块联邦的基本的用法.