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

模块联邦概述

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

以下图为例:

微前端解决方案初探 03 模块联邦(概述、案例介绍、应用初始化)_第1张图片

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

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

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

创建应用结构

需求

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

微前端解决方案初探 03 模块联邦(概述、案例介绍、应用初始化)_第2张图片

应用结构

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

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 += `
${faker.commerce.productName()}
`
} 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>Containertitle>
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 的组织名称
      // 被远程引用时路径为 `/`
      // 模块名称具有唯一性,不同的模块不能具有相同的名称,如果名称相同,可以在 `remotes` 配置引入的时候设置模块别名
      name: 'products',

      // 被远程引用时可暴露的资源路径及其别名
      // key 是模块名称
      // value 是具体的模块路径(`.js` 扩展名可以省略)
      exposes: {
        // 被远程引用时的路径为 `/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 是模块构建的文件地址
        products: 'products@http://localhost:8081/remoteEntry.js'
      }
    })
  ]
}

加载远程应用模块:

// container\src\index.js
// 因为 products 是远程应用模块,要发送请求,所以使用异步加载的方式
// products 是 remotes 中配置的模块别名,index 是产品列表微应用配置的具体资源名称
import('products/index').then(products => {
  const sayHello = products.default
  sayHello('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 模块联邦(概述、案例介绍、应用初始化)_第3张图片

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

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

正常打包流程

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

模块联邦插件打包流程

微前端解决方案初探 03 模块联邦(概述、案例介绍、应用初始化)_第4张图片

配置模块联邦插件的时候,通过 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 模块联邦(概述、案例介绍、应用初始化)_第5张图片

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 模块联邦(概述、案例介绍、应用初始化)_第6张图片

访问 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 是模块构建的文件地址
        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>Containertitle>
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

你可能感兴趣的:(#,微前端,前端,javascript,vue.js)