模块联邦

一、模块联邦概述

  1. 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 中最重要的属性是 nameversion 两个属性,这两个属性是必须要有的,否则模块就无法被安装,这两个属性一起形成了一个 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/,如图所示

源码:[https://gitee.com/caicai521/module-federation](

posted @ 2022-10-09 14:47  菜菜123521  阅读(368)  评论(0编辑  收藏  举报