模块联邦概述

模块联邦(Module Federation)是 Webpack 5 中新增的一项功能,可以实现跨应用共享模块。

以下图为例:

微前端解决方案初探 03 模块联邦(概述、案例介绍、应用初始化)-编程知识网

在 A 应用中有一个 sayHelloFromA 方法,在 B 应用中有一个 sayHelloFromB 方法。

如果要实现在 A 应用中调用 B 应用中的 sayHelloFromB 方法,在 B 应用中调用 A 应用的 sayHelloFromA 方法,这种跨应用调用方法的场景可以使用 模块联邦 实现。

将每个应用看作一个模块,在一个应用中加载另一个应用,这样就可以实现微前端架构。

创建应用结构

需求

稍后会创建 3 个应用,一个容器应用,两个微应用。最终要通过模块联邦的方式在容器应用中加载微应用。应用中全部使用 faker 创建假数据。

微前端解决方案初探 03 模块联邦(概述、案例介绍、应用初始化)-编程知识网

应用结构

这三个应用使用相同的结构,以产品列表应用为例:

products
├─ public # 静态资源文件
│   └─ index.html
├─ src # 应用源代码
│   ├─ bootstrap.js # 加载远程模块和执行业务逻辑的文件
│   └─ index.js # 应用入口文件
├─ package-lock.json # 项目工程文件
├─ package.json # 项目工程文件
└─ webpack.config.js # webpack 配置文件

应用初始搭建

# 创建 module-federations 文件夹存放所有应用
mkdir module-federations
cd module-federations# 创建 products 文件夹存放产品列表应用
mkdir products
cd products# 初始化 package.json
npm init -y# 安装依赖
npm i faker html-webpack-plugin webpack webpack-cli webpack-dev-server

本例安装的依赖版本如下:

"dependencies": {"faker": "^5.5.3","html-webpack-plugin": "^5.5.0","webpack": "^5.68.0","webpack-cli": "^4.9.2","webpack-dev-server": "^4.7.4"
}

新建模板文件 public/index.html

新建应用入口文件 src/index.js

新建 webpack.config.js 配置文件。

修改 package.json 的启动脚本:

"scripts": {"start": "webpack serve"
},

最后相同的步骤创建容器应用 container 和 购物车应用 cart。

产品微应用初始化

入口文件中加入产品列表

// products\src\index.js
import faker from 'faker'let products = ''for (let i = 0; i <= 5; i++) {// 随机生成产品名称products += `<div>${faker.commerce.productName()}</div>`
}document.querySelector('#dev-products').innerHTML = products// 导出一个方法
export default function sayHello(name) {console.log(`Hello ${name}`)
}

定义模板文件

<!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-products"></div>
</body>
</html>

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'})]
}

运行

npm start 运行应用,访问 http://localhost:8081 查看是否成功。

容器应用初始化

入口文件添加测试代码

// container\src\index.js
console.log('container')

定义模板文件

复制 products/public/index.html 文件到容器应用,并修改标题(注意保留同 id 的 div):

<!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>Container</title>
</head>
<body><div id="dev-products"></div>
</body>
</html>

webpack 配置

复制 products/webpack.config.js 文件到容器应用中,并修改启动端口:

// container\webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')module.exports = {mode: 'development',devServer: {port: 8080},plugins: [new HtmlWebpackPlugin({template: './public/index.html'})]
}

运行

npm start 运行后访问 http://localhost:8080

在容器应用中加载产品列表微应用

通过配置模块联邦实现在容器应用中加载产品列表微应用。

在产品列表为应用中将自身作为模块进行导出

// 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: [new HtmlWebpackPlugin({template: './public/index.html'}),// 将应用自身作为模块暴露出去new ModuleFederationPlugin({// 构建输出的模块文件名称// 其它应用引入当前模块时需要加载的文件名称filename: 'remoteEntry.js',// 模块名称,相当于 single-spa 的组织名称// 被远程引用时路径为 `<name>/<expose>`// 模块名称具有唯一性,不同的模块不能具有相同的名称,如果名称相同,可以在 `remotes` 配置引入的时候设置模块别名name: 'products',// 被远程引用时可暴露的资源路径及其别名// key 是模块名称// value 是具体的模块路径(`.js` 扩展名可以省略)exposes: {// 被远程引用时的路径为 `<name>/index`// 注意:模块名称前要添加 `./` 才会生效'./index': './src/index'}})]
}

在容器应用中导入产品列表微应用

在容器应用中引入产品列表微应用的方式:

  1. 在容器应用中加载产品列表微应用构建的模块文件
  2. 在容器应用中通过 import 关键字从模块文件中导入产品列表微应用模块
// 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 HtmlWebpackPlugin({template: './public/index.html'}),new ModuleFederationPlugin({name: 'container',// 远程引用的应用及其别名的映射remotes: {// key 是模块的别名,作为当前应用中引入该模块时的 name// value 是模块具体地址,有两部分组成:`<name>@<url>`//   name 是模块自己配置的名称//   url 是模块构建的文件地址products: 'products@http://localhost:8081/remoteEntry.js'}})]
}

加载远程应用模块:

// container\src\index.js
// 因为 products 是远程应用模块,要发送请求,所以使用异步加载的方式
// products 是 remotes 中配置的模块别名,index 是产品列表微应用配置的具体资源名称
import('products/index').then(products => {const sayHello = products.defaultsayHello('container')
})

重新启动

重新启动 products 和 container 应用,访问 http:localhost:8080 看是否显示了产品列表。

加载方式优化

webpack 关于使用 bootstrap.js 的介绍

在入口文件中加载远程应用模块,需要异步加载,在回调中执行全部逻辑。

可以将加载远程应用模块和逻辑,以同步的形式写在另一个文件中,在入口文件中异步加载这个文件,解决嵌套一层回调的问题。

创建一个 bootstrap.js 文件:

// container\src\bootstrap.js
// 已同步的方式引入远程应用模块
import sayHello from 'products/index'sayHello('container-bootstrap')

修改入口文件:

// container\src\index.js
// 因为 products 是远程应用模块,要发送请求,所以使用异步加载的方式
// products 是 remotes 中配置的模块别名,index 是产品列表微应用配置的具体资源名称
// import('products/index').then(products => {
//   const sayHello = products.default
//   sayHello('container')
// })// 异步加载
import('./bootstrap')

应用 webpack 打包分析和容器应用文件加载顺序分析

Products 应用打包分析

微前端解决方案初探 03 模块联邦(概述、案例介绍、应用初始化)-编程知识网

products 应用中只有一个入口文件 index.js,webpack 在对这个应用打包的时候会执行两个流程:

  • 正常打包流程
  • 模块联邦插件的打包流程

正常打包流程

正常打包流程,最终会构建生成 main.js(默认)的文件,允许我们单独运行应用。

模块联邦插件打包流程

微前端解决方案初探 03 模块联邦(概述、案例介绍、应用初始化)-编程知识网

配置模块联邦插件的时候,通过 filename 选项指定了当前应用模块的文件名称为 remoteEntry.js,模块联邦插件会将应用模块打包成这个文件。

该文件中包含模块中需要加载的文件列表,以及如何加载它们的代码。

另外还通过 exposes 选项配置了具体资源文件,即当前应用模块要导出的模块列表。

模块联邦插件会将这个模块列表中的文件打包成单独的文件,例如当前 products 应用中会将 ./src/index.js 打包成名为 src_index_js.js 的文件(图中以 src_index.js 表示),这个文件中包含 ./src/index.js 文件的代码。

./src/index.js 文件中又引入了 faker 模块,该模块对应的文件地址是 node_modules/faker/index.js,所以模块联邦插件也会将它单独打包为名为(默认前缀) vendors-node_modules_faker_index_js.js 的文件(图中以 faker.js 表示)。

Container 应用打包分析

微前端解决方案初探 03 模块联邦(概述、案例介绍、应用初始化)-编程知识网

container 应用中包含两个文件:

  • src/index.js:入口文件,异步加载了 src/bootstrap.js 文件
  • src/bootstrap.js:同步加载 products 远程模块

webpack 在对 container 应用打包的时候,最终会生成两个文件:

  • main.js:包含 src/index.js 文件的内容
  • bootstrap.js:包含 src/bootstrap.js 文件的内容

由于 products 模块是远程模块,需要异步加载,所以不会被打包为本地文件。

文件加载顺序分析

微前端解决方案初探 03 模块联邦(概述、案例介绍、应用初始化)-编程知识网

访问 container 应用(http://localhost:8080) 查看文件加载顺序。

  • 首先会加载该应用的 main.js 文件
  • 加载 main.js 文件时发现需要加载 bootstrap.js
  • 加载 bootstrap.js 的时候发现需要从 products 模块中导入一些内容
  • 首先找到 products 对应的模块文件 remoteEntry.js,该文件包含如何加载模块的代码:加载 src_index.jsfaker.js
  • src_index.jsfaker.js 两个文件加载完成后,bootstrap.js 就完成了 products 模块的加载,具备了执行的条件
  • 于是 bootstrap.js 执行了 import sayHello from 'products/index' 的操作,并执行后面的代码

购物车微应用初始化

下面使用相同的方式在 Container 应用中加载 cart 应用。

入口文件

// cart\src\index.js
import faker from 'faker'// 生成随机数
document.querySelector('#dev-cart').innerHTML = `在您的购物车中有${faker.datatype.number()}件商品`

模板文件

<!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>

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 HtmlWebpackPlugin({template: './public/index.html'}),new ModuleFederationPlugin({filename: 'remoteEntry.js',name: 'cart',exposes: {'./index': './src/index'}})]
}

在容器应用中加载 cart 应用

在容器应用中配置 cart 应用:

// 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 HtmlWebpackPlugin({template: './public/index.html'}),new ModuleFederationPlugin({name: 'container',// 远程引用的应用及其别名的映射remotes: {// key 是模块的别名,作为当前应用中引入该模块时的 name// value 是模块具体地址,有两部分组成:`<name>@<url>`//   name 是模块自己配置的名称//   url 是模块构建的文件地址products: 'products@http://localhost:8081/remoteEntry.js',cart: 'cart@http://localhost:8082/remoteEntry.js'}})]
}

模板文件中添加 cart 应用挂载节点:

<!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>Container</title>
</head>
<body><div id="dev-products"></div><div id="dev-cart"></div>
</body>
</html>

引入 cart 模块:

// container\src\bootstrap.js
// 已同步的方式引入远程应用模块
import sayHello from 'products/index'
import 'cart/index'sayHello('container-bootstrap')

现在重新运行 container 和 cart 应用,再次访问 `http://localhost:8080