如何利用 `Webpack` 联邦模块(`Module Federation`),实现 Vue 微前端架构下的代码共享?

各位观众,大家好!我是今天的主讲人,很高兴能和大家一起探讨 Vue 微前端架构下,如何利用 Webpack 联邦模块实现代码共享这个话题。 今天咱们不说那些高大上的理论,直接撸起袖子,用最通俗易懂的语言,把这个看似复杂的技术拆解开来,让大家都能听明白,学得会,用得上。

开场白:微前端,共享的渴望

想象一下,你是一家大型公司的前端负责人,手下有十几个团队,每个团队都在开发自己的 Vue 应用。这些应用功能相似,比如都有用户登录、权限管理、通用组件等等。如果没有微前端,那每个团队就得重复造轮子,维护着相似的代码,这效率,简直让人抓狂。

微前端的出现,就是为了解决这个问题。它把一个大型应用拆分成多个小型应用,每个应用独立开发、独立部署,但最终又能像一个整体一样运行。 而联邦模块,就是微前端架构下实现代码共享的利器。

联邦模块:共享代码的魔法

联邦模块,简单来说,就是让不同的 Webpack 构建的应用,可以相互导入对方的代码。就像搭积木一样,每个应用都是一个积木块,你可以把其他应用的积木块拿过来,拼到自己的应用里。

这听起来很神奇,但其实原理并不复杂。Webpack 会把需要共享的代码打包成一个模块,然后发布出去。其他应用可以通过配置,引入这个模块,就像引入一个普通的 npm 包一样。

实战演练:搭建一个简单的微前端应用

为了让大家更直观地理解联邦模块,我们来搭建一个简单的微前端应用。这个应用包含两个子应用:app1app2app1 提供一个通用的按钮组件,app2 引入这个按钮组件,并在自己的页面上使用。

步骤 1:创建两个 Vue 项目

首先,我们使用 Vue CLI 创建两个 Vue 项目:

vue create app1
vue create app2

步骤 2:配置 app1,暴露按钮组件

进入 app1 项目,安装 webpack@module-federation/webpack-plugin

cd app1
npm install webpack @module-federation/webpack-plugin -D

app1 项目的根目录下,创建一个 vue.config.js 文件,并添加以下配置:

const { defineConfig } = require('@vue/cli-service')
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: 'http://localhost:8080/', // 注意这里的publicPath,要与运行的端口号一致。
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'app1', // 必须全局唯一
        filename: 'remoteEntry.js', // 远程模块入口文件名
        exposes: {
          './MyButton': './src/components/MyButton.vue', // 暴露的模块
        },
        // shared: ['vue'] // 共享的依赖,这里可以共享vue,避免重复打包
        shared: {
          vue: {
            singleton: true,
            requiredVersion: '^3.0.0'
          }
        }
      }),
    ],
  },
  devServer: {
    port: 8080, //app1 的端口号,注意与publicPath一致
  },
})
  • name: 应用的名称,必须全局唯一,相当于应用的 ID。
  • filename: 远程模块入口文件名,其他应用可以通过这个文件找到 app1 暴露的模块。
  • exposes: 定义了要暴露的模块,./MyButton 是模块的别名,./src/components/MyButton.vue 是模块的实际路径。
  • shared: 定义了要共享的依赖,这里我们共享了 vue,避免重复打包。singleton: true 表示只使用一个 vue 实例,requiredVersion: '^3.0.0' 表示需要的 vue 版本。

接下来,在 app1 项目的 src/components 目录下,创建一个 MyButton.vue 文件,内容如下:

<template>
  <button class="my-button">{{ text }}</button>
</template>

<script>
export default {
  props: {
    text: {
      type: String,
      default: 'Button',
    },
  },
};
</script>

<style scoped>
.my-button {
  padding: 10px 20px;
  background-color: #4CAF50;
  color: white;
  border: none;
  cursor: pointer;
}
</style>

步骤 3:配置 app2,引入按钮组件

进入 app2 项目,安装 webpack@module-federation/webpack-plugin

cd app2
npm install webpack @module-federation/webpack-plugin -D

app2 项目的根目录下,创建一个 vue.config.js 文件,并添加以下配置:

const { defineConfig } = require('@vue/cli-service')
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: 'http://localhost:8081/', // 注意这里的publicPath,要与运行的端口号一致。
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'app2', // 必须全局唯一
        remotes: {
          app1: 'app1@http://localhost:8080/remoteEntry.js', // 远程应用地址
        },
        // shared: ['vue'] // 共享的依赖,这里可以共享vue,避免重复打包
        shared: {
          vue: {
            singleton: true,
            requiredVersion: '^3.0.0'
          }
        }
      }),
    ],
  },
  devServer: {
    port: 8081, //app2 的端口号,注意与publicPath一致
  },
})
  • remotes: 定义了要引入的远程应用,app1 是远程应用的别名,app1@http://localhost:8080/remoteEntry.js 是远程应用的地址。 注意这里的 app1@ 不能少,它表示 app1 这个远程应用的 name

现在,修改 app2 项目的 src/App.vue 文件,引入并使用 app1MyButton 组件:

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <MyButton text="Hello from app1" />
</template>

<script>
import { defineAsyncComponent } from 'vue'

export default {
  components: {
    MyButton: defineAsyncComponent(() => import('app1/MyButton')),
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
  • defineAsyncComponent 是 Vue 3 提供的一个异步组件加载函数。 因为联邦模块的加载是异步的,所以我们需要使用 defineAsyncComponent 来加载远程组件。
  • import('app1/MyButton') app1 是远程应用的别名,MyButtonapp1 暴露的模块的别名。

步骤 4:启动两个应用

分别进入 app1app2 项目,启动开发服务器:

cd app1
npm run serve

cd app2
npm run serve

打开浏览器,访问 http://localhost:8081,你应该能看到 app2 的页面,并且页面上显示了 app1MyButton 组件。

代码共享的多种姿势

上面的例子只是一个简单的演示,联邦模块的功能远不止于此。 我们可以共享组件、共享工具函数、共享状态管理等等。 下面我们来探讨几种常见的代码共享姿势:

  • 共享组件库: 将通用的 UI 组件,例如按钮、输入框、表格等等,打包成一个组件库,供所有应用使用。 这样可以保证 UI 风格的统一,减少重复开发。

  • 共享工具函数: 将常用的工具函数,例如日期格式化、字符串处理、数据校验等等,打包成一个工具函数库,供所有应用使用。 这样可以提高代码的复用率,减少代码冗余。

  • 共享状态管理: 将通用的状态管理逻辑,例如用户登录状态、权限信息等等,打包成一个状态管理模块,供所有应用使用。 这样可以保证状态的一致性,减少状态管理的复杂性。

共享策略:如何避免冲突?

在共享代码的过程中,最常见的问题就是依赖冲突。 比如,app1 使用了 [email protected],而 app2 使用了 [email protected],这时候就会发生冲突。 为了解决这个问题,我们需要制定一套合理的共享策略。

共享策略 优点 缺点
完全共享 节省带宽,减少代码体积,保证代码的一致性。 如果不同应用使用的依赖版本不兼容,可能会导致运行时错误。
版本范围共享 允许不同应用使用不同版本的依赖,只要版本在指定的范围内即可。 增加了代码体积,可能会导致代码冗余。
独立打包 每个应用都独立打包自己的依赖,互不影响。 浪费带宽,增加代码体积,可能会导致代码冗余。

webpack.config.js 文件的 shared 字段中,我们可以配置共享策略。 比如:

shared: {
  vue: {
    singleton: true,
    requiredVersion: '^3.0.0', // 允许使用 3.0.0 及以上版本的 vue
  },
  lodash: {
    version: '4.17.21', // 强制使用 4.17.21 版本的 lodash
  },
}

调试技巧:如何排查问题?

在使用联邦模块的过程中,难免会遇到一些问题。 比如,模块加载失败、依赖冲突等等。 这时候,我们需要掌握一些调试技巧,才能快速定位问题。

  • 查看 Webpack 构建日志: Webpack 构建日志包含了大量的调试信息,可以帮助我们了解模块的加载过程、依赖关系等等。

  • 使用 Webpack Devtool: Webpack Devtool 可以帮助我们调试 Webpack 构建的代码,查看模块的源代码、依赖关系等等。

  • 使用浏览器开发者工具: 浏览器开发者工具可以帮助我们调试运行时的代码,查看模块的加载情况、错误信息等等。

高级技巧:动态模块加载

除了静态模块加载,联邦模块还支持动态模块加载。 动态模块加载是指在运行时根据需要加载模块,而不是在构建时就加载所有模块。

动态模块加载可以减少初始加载时间,提高应用的性能。 比如,我们可以根据用户的权限,动态加载不同的模块。

总结:联邦模块的优势与挑战

联邦模块是一种强大的代码共享工具,可以帮助我们构建可扩展、可维护的微前端应用。 但是,联邦模块也存在一些挑战,比如依赖冲突、模块加载性能等等。

优势 挑战
代码共享,减少重复开发 依赖冲突,需要制定合理的共享策略
独立部署,降低部署风险 模块加载性能,需要优化模块加载策略
可扩展性,方便添加新功能 调试难度,需要掌握一定的调试技巧
技术栈无关性,可以使用不同的技术栈开发不同的微应用 学习成本,需要了解 Webpack 和联邦模块的配置

总的来说,联邦模块是一种非常有价值的技术,值得我们深入学习和应用。

结束语:共享,让前端更美好

今天的讲座就到这里,希望大家通过今天的学习,能够掌握联邦模块的基本原理和使用方法,并在实际项目中应用起来。

记住,代码共享是一种美德,它可以让我们的前端世界更加美好! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注