1. 概述

模块联邦(Module Federation)是Webpack 5中新增的一项功能,可以实现跨应用共享模块。比如有AB两个应用,A应用中提供fromA方法,B应用提供了fromB方法,现在需要在A应用中调用B应用的方法,B应用中调用A应用的方法。这就涉及到了跨应用调用方法,就需要用到模块联邦啦。通过这样的方式就可以实现微前端啦。

这里通过一个案例进行演示,创建三个应用,一个容器应用,两个微应用,在容器应用中使用这两个微应用,项目结构如下。三个应用的结构基本相同。

products
    ├── package-lock.json
    ├── package.json
    ├── public
    │   └── index.html
    ├── src
    │   └── index.js
    └── webpack.config.js

2. 案例演示

{
  "name": "container",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "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"
  }
}

在入口JavaScript文件中加入产品列表也就是src/index.js文件。首先引入faker,他是用来随机生成数据的,不用自己造数据了,然后将生成的数据添加到页面中。

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

在入口html文件中加入渲染的div

<div id="dev-products"></div>

webpack 配置。

const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
  mode: "development",
  devServer: {
    port: 8081 
    },
    plugins: [
            new HtmlWebpackPlugin({
                template: "./public/index.html"
            })
    ] 
}

应用启动之后会运行在8081端口。

3. 使用模块联邦

要使用模块联邦首先需要导入ModuleFederationPlugin模块,他是webpack内置的模块,不需要额外安装。

可以通过new实力换模块联邦插件,可以传入一个配置项。filename模块文件名称,name是模块名称,exposes指定导出的文件。

// webpack.config.js
// 导入模块联邦插件
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
// 将 products 自身当做模块暴露出去 
new ModuleFederationPlugin({
    // 模块文件名称, 其他应用引入当前模块时需要加载的文件的名字 
    filename: "remoteEntry.js",
    // 模块名称, 具有唯一性, 相当于 single-spa 中的组织名称 
    name: "products",
    // 当前模块具体导出的内容 
    exposes: {
        "./index": "./src/index"
    }
})

在容器应用的中导入产品列表微应用,同样实例化ModuleFederationPlugin传入参数。remotes是要引入的模块。@前面是模块name,后面是remoteEntry.js就是filename

// webpack.config.js
// 导入模块联邦插件
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
new ModuleFederationPlugin({ 
    name: "container",
    // 配置导入模块映射
    remotes: {
        // 字符串 "products" 和被导入模块的 name 属性值对应
        // 属性 products 是映射别名, 是在当前应用中导入该模块时使用的名字 
        products: "products@http://localhost:8081/remoteEntry.js"
    }
})

配置之后就可以加载微应用了,通过import加载。products就是remotes中的remotesindex就是微应用exposes中的index

// 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"

4. 打包分析

接着看下Container应用和products应用webpack打包流程。在products应用中只有一个文件index.jswebpack打包的时候会走两个流程正常流程和模块联邦流程。

正常的打包流程会生成main.js文件,也就是最终的打包文件,这是正常运行webpack打包应用生成的文件。模块联邦插件会把设置的remoteEntry.js打包成单独的文件,在这个文件中包含文件需要加载的列表和如何加载模块的代码。exposes是模块要导出的列表,模块联邦会把列表中导出的插件打包成单独的文件,比如说导出了index就会把index对应的文件打包成单独的文件,文件名称就是src_index.js,也就是文件路径。

在模块联邦中也用到了faker插件,所以也会把faker.js单独打包成一个文件。这就是products的打包流程。

container应用打包包含两个文件,index.jsbootstrap.js,在index.js中通过import导入了bootstrap.js,在bootstrap.js又导入了products模块,webpack对应用打包的时候最终会生成两个文件,main.jsbootstrap.jsmain.js就是包含的index.js中的文件内容。products文件不会打包,会通过异步方式引入进来。

Container应用在执行的时候首先会加载main.js并执行,因为里面包含了bootstrap.js所以也会加载bootstrap.js,但是只是加载,并没有执行,bootstrap.js又导入了products同样进行加载remoteEntry.jsfaker.js。加载完成之后就可以执行bootstrap.js文件了。

5. 共享模块

ProductsCart中都需要Faker,当Container加载了这两个模块后,Faker被加载了两次。

// 分别在 Products 和 Cart 的 webpack 配置文件中的模块联邦插件中添加以下代码 
{
  shared: ["faker"] // 共享模块的名字,可以写多个。
}
// 重新启动 Container、Products、Cart

共享模块需要异步加载,在ProductsCart中需要添加bootstrap.js

6. 解决冲突

Cart中如果使用4.1.0版本的fakerProducts中使用5.2.0版本的faker,通过查看网络控制面板可以发现faker又会被加载了两次,模块共享失败。

解决办法是分别在ProductsCart中的webpack配置中加入如下代码。

shared: {
  faker: {
    singleton: true // 如果版本不一致使用高版本
  }
}

但同时会在原本使用低版本的共享模块应用的控制台中给予警告提示。

7. 子应用挂载接口

在容器应用导入微应用后,应该有权限决定微应用的挂载位置,而不是微应用在代码运行时直接进行挂载。所以每个微应用都应该导出一个挂载方法供容器应用调用。

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

转载须知

如转载必须标明文章出处文章名称文章作者,格式如下:

转自:【致前端 - zhiqianduan.com】 模块联邦简介  "隐冬"
请输入评论...