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

各位观众老爷,大家好!今天咱们不开车,聊点技术。相信大家或多或少都听过“微前端”这个词,它就像前端界的变形金刚,能把一个庞大的应用拆解成一个个小而精悍的模块,独立开发、独立部署,最后再组装起来。这好处嘛,那是杠杠的,团队协作更高效,代码维护更轻松,简直就是程序员的福音!

而要实现微前端,其中一个非常流行的技术就是 Webpack 的 Module Federation(模块联邦)。 它允许我们将应用拆分成独立的构建单元,这些单元可以动态地在运行时共享代码。 听起来是不是有点玄乎? 别怕,今天我就把这个“玄学”的东西,用大白话掰开了揉碎了讲清楚,保证你听完之后,也能玩转 Module Federation,让你的 Vue 项目飞起来!

咱们今天就以一个实际的 Vue 项目为例,手把手教你如何配置 Webpack 的 Module Federation,实现微前端架构下的代码共享。

一、 啥是 Module Federation? 为什么要用它?

在开始配置之前,咱们先来聊聊 Module Federation 到底是个啥玩意儿。 简单来说,它就是 Webpack 提供的一种代码共享机制,允许不同的 Webpack 构建之间共享模块。 想象一下,你有一个组件库,里面有很多常用的组件,如果每个微前端应用都自己维护一份,那得多浪费啊! 有了 Module Federation,我们就可以把这个组件库作为一个“远程模块”,让其他应用直接引用,既节省了空间,又保证了代码的一致性。

用表格来总结一下 Module Federation 的优点:

优点 描述
代码共享 不同的应用可以共享代码,避免重复开发。
独立部署 每个微前端应用都可以独立部署,互不影响。
运行时集成 模块的加载和集成发生在运行时,更加灵活。
技术栈无关 可以与其他微前端技术(如 iframe、single-spa)结合使用。

二、 项目准备: 三个“小弟”

为了演示 Module Federation,我们需要准备三个 Vue 项目:

  1. app-shell (主应用): 负责应用的整体布局和路由管理。
  2. app-one (微应用 1): 一个简单的计数器应用。
  3. app-two (微应用 2): 显示用户信息和调用 app-one 中计数器的一个应用。

你可以使用 Vue CLI 来快速创建这些项目:

vue create app-shell
vue create app-one
vue create app-two

每个项目都选择默认配置即可。

三、 app-shell (主应用) 配置

app-shell 作为主应用,负责加载和渲染其他的微应用。 它的配置稍微复杂一些,需要安装 webpack@module-federation/webpack-plugin

cd app-shell
npm install webpack @module-federation/webpack-plugin -D

然后,在 vue.config.js 文件中配置 Webpack:

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

module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: 'http://localhost:8080/', // 端口号要和你的项目一致
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'app_shell', // 必须唯一,作为远程模块的名称
        remotes: {
          app_one: 'app_one@http://localhost:8081/remoteEntry.js', // 远程模块的名称和地址
          app_two: 'app_two@http://localhost:8082/remoteEntry.js',
        },
        shared: ['vue', 'vue-router'] // 共享的依赖
      })
    ]
  },
  devServer: {
    port: 8080,
  }
})

这段代码做了什么呢?

  • name: 定义了当前应用的名称,其他应用可以通过这个名称来引用它。
  • remotes: 定义了需要引用的远程模块,包括模块的名称和 remoteEntry.js 的地址。 remoteEntry.js 是由远程模块暴露出来的入口文件,包含了模块的信息。
  • shared: 定义了需要共享的依赖,比如 vuevue-router。 这样可以避免重复加载,减少应用的大小。

重要提示: publicPath 的配置非常重要,它决定了应用在运行时加载资源的路径。 在开发环境下,我们通常使用 localhost 加端口号,而在生产环境下,则需要配置为实际的域名或 CDN 地址。

接下来,我们需要在 app-shell 中创建一个组件,用于加载和渲染微应用。 例如,我们可以创建一个名为 MicroApp.vue 的组件:

<template>
  <div class="micro-app">
    <h2>{{ name }}</h2>
    <component :is="component" />
  </div>
</template>

<script>
import { defineAsyncComponent } from 'vue'

export default {
  props: {
    name: {
      type: String,
      required: true
    },
    module: {
      type: String,
      required: true
    },
    scope: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      component: null
    }
  },
  mounted() {
    this.loadModule()
  },
  methods: {
    async loadModule() {
      try {
        this.component = defineAsyncComponent(() =>
          import(/* webpackChunkName: "[request]" */ `${this.scope}/${this.module}`)
        )
      } catch (error) {
        console.error('Failed to load module:', error)
      }
    }
  }
}
</script>

<style scoped>
.micro-app {
  border: 1px solid #ccc;
  padding: 10px;
  margin-bottom: 10px;
}
</style>

这个组件使用了 defineAsyncComponent 来异步加载远程模块。 scopemodule 属性分别对应 webpack.config.jsremotes 的键名和要暴露的组件名.

最后,在 app-shellApp.vue 中使用 MicroApp 组件:

<template>
  <div id="app">
    <h1>Main App (app-shell)</h1>
    <MicroApp name="App One" scope="app_one" module="./Counter" />
    <MicroApp name="App Two" scope="app_two" module="./UserInfo" />
  </div>
</template>

<script>
import MicroApp from './components/MicroApp.vue'

export default {
  components: {
    MicroApp
  }
}
</script>

四、 app-one (微应用 1) 配置

app-one 作为微应用,需要暴露一个组件供其他应用使用。 同样,我们需要安装 webpack@module-federation/webpack-plugin

cd app-one
npm install webpack @module-federation/webpack-plugin -D

然后在 vue.config.js 文件中配置 Webpack:

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

module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: 'http://localhost:8081/', // 端口号要和你的项目一致
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'app_one', // 必须唯一,作为远程模块的名称
        filename: 'remoteEntry.js', // 暴露的入口文件名称
        exposes: {
          './Counter': './src/components/Counter.vue' // 暴露的模块
        },
        shared: ['vue'] // 共享的依赖
      })
    ]
  },
  devServer: {
    port: 8081,
  }
})

这段代码的关键在于 exposes 属性,它定义了需要暴露的模块,以及对应的文件路径。 在这里,我们将 src/components/Counter.vue 组件暴露为 ./Counter 模块。

接下来,创建一个简单的计数器组件 src/components/Counter.vue:

<template>
  <div>
    <h2>Counter (app-one)</h2>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    const increment = () => {
      count.value++
    }

    return {
      count,
      increment
    }
  }
}
</script>

五、 app-two (微应用 2) 配置

app-two 的配置与 app-one 类似,也需要安装 webpack@module-federation/webpack-plugin

cd app-two
npm install webpack @module-federation/webpack-plugin -D

然后在 vue.config.js 文件中配置 Webpack:

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

module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: 'http://localhost:8082/', // 端口号要和你的项目一致
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: 'app_two', // 必须唯一,作为远程模块的名称
        filename: 'remoteEntry.js', // 暴露的入口文件名称
        exposes: {
          './UserInfo': './src/components/UserInfo.vue' // 暴露的模块
        },
        remotes: {
          app_one: 'app_one@http://localhost:8081/remoteEntry.js',
        },
        shared: ['vue'] // 共享的依赖
      })
    ]
  },
  devServer: {
    port: 8082,
  }
})

注意,app-two 除了暴露 UserInfo 组件外,还引用了 app_oneCounter 组件。

接下来,创建 src/components/UserInfo.vue 组件:

<template>
  <div>
    <h2>User Info (app-two)</h2>
    <p>Name: John Doe</p>
    <p>Email: [email protected]</p>
    <Counter />
  </div>
</template>

<script>
import Counter from 'app_one/Counter' // 引用 app-one 的 Counter 组件

export default {
  components: {
    Counter
  }
}
</script>

六、 启动项目,见证奇迹!

现在,我们可以分别启动这三个项目:

cd app-shell
npm run serve

cd app-one
npm run serve

cd app-two
npm run serve

打开 http://localhost:8080/,你应该可以看到主应用 app-shell 成功加载了 app-oneCounter 组件和 app-twoUserInfo 组件。 在 app-two 中,你还可以看到 app-oneCounter 组件。

七、 踩坑指南

在配置 Module Federation 的过程中,你可能会遇到一些坑,这里给大家总结一下:

  • publicPath 配置错误: publicPath 决定了应用加载资源的路径,如果配置错误,会导致资源加载失败。 确保每个应用的 publicPath 都指向正确的地址。
  • 依赖版本冲突: 如果不同的应用使用了不同版本的共享依赖,可能会导致运行时错误。 建议使用 shared 属性来共享依赖,并尽量保持依赖版本一致。
  • 模块名称冲突: 如果不同的应用暴露了相同名称的模块,会导致模块加载失败。 确保每个应用的模块名称都是唯一的。
  • 循环依赖: 避免出现循环依赖的情况,例如 app-one 依赖 app-two,而 app-two 又依赖 app-one。 这样会导致模块加载死循环。
  • CORS 问题: 如果你的微前端应用部署在不同的域名下,可能会遇到 CORS 问题。 你需要在服务器端配置 CORS 策略,允许跨域访问。

八、 更高级的玩法

Module Federation 的玩法还有很多,例如:

  • 动态加载远程模块: 可以根据用户的行为或路由的变化,动态加载不同的远程模块。
  • 共享状态管理: 可以使用 Redux、Vuex 等状态管理工具,在不同的微前端应用之间共享状态。
  • 按需加载: 可以使用 Webpack 的代码分割功能,将远程模块拆分成更小的 chunk,按需加载,提高应用的性能。
  • 版本控制: 通过维护不同版本的 remoteEntry.js,可以实现微前端应用的版本控制和灰度发布。

九、 总结

今天,我们一起学习了如何使用 Webpack 的 Module Federation 来构建微前端应用。 希望通过今天的讲解,你能够掌握 Module Federation 的基本概念和配置方法,并在实际项目中灵活运用。

记住,技术是用来解决问题的,不要为了用技术而用技术。 在选择微前端架构时,一定要根据项目的实际情况,权衡利弊,选择最适合你的方案。

好了,今天的分享就到这里。 希望对大家有所帮助! 咱们下次再见!

发表回复

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