webpack5模块联邦-实例实现

518 阅读1分钟

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"
  }
})

在容器应用中要如何引入产品列表应用模块?

  1. 在容器应用中加载产品列表应用的模块文件
  2. 在容器应用中通过 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"))

至此,你已经了解了模块联邦的基本的用法.