Vue 3 与 Module Federation

概述

Module Federation(模块联邦)是 Webpack 5 引入的一项革命性特性,它允许在不同的 Webpack 构建之间共享代码和资源,无需额外的依赖管理或构建步骤。这一特性为微前端架构提供了全新的实现方式,使得不同团队可以独立开发、构建和部署应用模块,同时在运行时无缝集成。Vue 3 凭借其灵活的组件系统和组合式 API,与 Module Federation 结合使用时能够发挥出强大的威力。

本集将深入探讨 Module Federation 的核心概念、工作原理以及如何在 Vue 3 应用中实现 Module Federation。我们将学习如何创建联邦应用、共享组件和状态、处理依赖共享等高级技巧,帮助你构建更加灵活、可扩展的 Vue 3 应用。

核心知识点

1. Module Federation 基本概念

1.1 什么是 Module Federation

Module Federation 是 Webpack 5 引入的一种模块共享机制,它允许一个应用(称为 host)动态加载另一个应用(称为 remote)的模块,而无需将这些模块打包到 host 应用中。这种方式打破了传统的代码共享模式,使得不同团队可以独立开发和部署应用模块,同时在运行时无缝集成。

1.2 核心术语

  • Host(宿主应用):加载和使用远程模块的应用
  • Remote(远程应用):提供可共享模块的应用
  • Exposes(暴露):Remote 应用中声明的可共享模块
  • Remotes(远程引用):Host 应用中声明的对 Remote 应用的引用
  • Shared(共享):Host 和 Remote 应用之间共享的依赖
  • Module(模块):可以是组件、工具函数、状态管理库等任何可导出的 JavaScript 模块

1.3 工作原理

Module Federation 的工作原理基于以下几个核心机制:

  1. Remote Entry 文件:Remote 应用构建时会生成一个 remoteEntry.js 文件,该文件包含了 Remote 应用暴露的模块信息和加载逻辑
  2. 动态加载:Host 应用通过加载 Remote 应用的 remoteEntry.js 文件,获取 Remote 应用暴露的模块信息
  3. 模块加载:当 Host 应用需要使用 Remote 应用的模块时,会通过 Webpack 的 Runtime 动态加载该模块
  4. 依赖共享:Host 和 Remote 应用可以共享依赖,避免重复加载相同的依赖库

2. Vue 3 与 Module Federation 集成

2.1 环境准备

首先,我们需要创建两个 Vue 3 应用:一个作为 Host 应用,一个作为 Remote 应用。

# 创建 Host 应用
npm create vite@latest federation-host -- --template vue
cd federation-host
npm install

# 返回上一级目录
cd ..

# 创建 Remote 应用
npm create vite@latest federation-remote -- --template vue
cd federation-remote
npm install

2.2 安装 Module Federation 插件

Module Federation 是 Webpack 5 的内置特性,但在 Vite 中需要使用插件来实现。我们将使用 @originjs/vite-plugin-federation 插件。

# 在 Host 应用中安装
npm install @originjs/vite-plugin-federation -D

# 在 Remote 应用中安装
npm install @originjs/vite-plugin-federation -D

2.3 配置 Remote 应用

在 Remote 应用中,我们需要配置要暴露的模块。

// federation-remote/vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'federation_remote', // Remote 应用名称
      filename: 'remoteEntry.js', // Remote Entry 文件名
      exposes: {
        // 暴露的模块
        './RemoteComponent': './src/components/RemoteComponent.vue',
        './sharedStore': './src/stores/sharedStore.js',
        './utils': './src/utils/index.js'
      },
      shared: ['vue'] // 共享的依赖
    })
  ],
  server: {
    port: 5174, // Remote 应用端口
    headers: {
      'Access-Control-Allow-Origin': '*' // 允许跨域访问
    }
  },
  build: {
    modulePreload: false,
    target: 'esnext',
    minify: false,
    cssCodeSplit: false
  }
})

创建要暴露的组件、状态管理和工具函数:

<!-- federation-remote/src/components/RemoteComponent.vue -->
<template>
  <div class="remote-component">
    <h3>Remote Component</h3>
    <p>This component is shared via Module Federation.</p>
    <button @click="incrementCount">Count: {{ count }}</button>
    <p>{{ message }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
const message = ref('Hello from Remote Component!')

const incrementCount = () => {
  count.value++
}
</script>

<style scoped>
.remote-component {
  background-color: #f0f0f0;
  padding: 16px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

button {
  background-color: #42b983;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #369f71;
}
</style>
// federation-remote/src/stores/sharedStore.js
import { ref, computed } from 'vue'

export const useSharedStore = () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  
  const increment = () => {
    count.value++
  }
  
  const decrement = () => {
    count.value--
  }
  
  return {
    count,
    doubleCount,
    increment,
    decrement
  }
}
// federation-remote/src/utils/index.js
export const formatDate = (date) => {
  return new Date(date).toLocaleDateString()
}

export const formatCurrency = (amount) => {
  return new Intl.NumberFormat('zh-CN', {
    style: 'currency',
    currency: 'CNY'
  }).format(amount)
}

2.4 配置 Host 应用

在 Host 应用中,我们需要配置对 Remote 应用的引用。

// federation-host/vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'federation_host', // Host 应用名称
      remotes: {
        // 远程应用引用
        'federation_remote': 'http://localhost:5174/assets/remoteEntry.js'
      },
      shared: ['vue'] // 共享的依赖
    })
  ],
  server: {
    port: 5173 // Host 应用端口
  },
  build: {
    modulePreload: false,
    target: 'esnext',
    minify: false,
    cssCodeSplit: false
  }
})

2.5 在 Host 应用中使用 Remote 模块

现在,我们可以在 Host 应用中使用 Remote 应用暴露的模块了。

<!-- federation-host/src/App.vue -->
<template>
  <div class="app">
    <h1>Vue 3 Module Federation Host App</h1>
    
    <!-- 使用远程组件 -->
    <h2>Remote Component:</h2>
    <RemoteComponent />
    
    <!-- 使用远程状态管理 -->
    <h2>Shared Store:</h2>
    <div class="shared-store">
      <p>Count: {{ count }}</p>
      <p>Double Count: {{ doubleCount }}</p>
      <div class="buttons">
        <button @click="increment">Increment</button>
        <button @click="decrement">Decrement</button>
      </div>
    </div>
    
    <!-- 使用远程工具函数 -->
    <h2>Remote Utils:</h2>
    <div class="utils">
      <p>Formatted Date: {{ formattedDate }}</p>
      <p>Formatted Currency: {{ formattedCurrency }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

// 动态导入远程组件
const RemoteComponent = () => import('federation_remote/RemoteComponent')

// 动态导入远程状态管理
const { useSharedStore } = await import('federation_remote/sharedStore')
const { count, doubleCount, increment, decrement } = useSharedStore()

// 动态导入远程工具函数
const { formatDate, formatCurrency } = await import('federation_remote/utils')

// 使用远程工具函数
const formattedDate = ref('')
const formattedCurrency = ref('')

onMounted(() => {
  formattedDate.value = formatDate(new Date())
  formattedCurrency.value = formatCurrency(1000)
})
</script>

<style>
.app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

h1 {
  color: #333;
  text-align: center;
}

h2 {
  color: #555;
  margin-top: 30px;
}

.shared-store {
  background-color: #f9f9f9;
  padding: 16px;
  border-radius: 8px;
  margin: 16px 0;
}

.buttons {
  margin-top: 16px;
}

button {
  background-color: #42b983;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  margin-right: 8px;
}

button:hover {
  background-color: #369f71;
}

.utils {
  background-color: #f9f9f9;
  padding: 16px;
  border-radius: 8px;
  margin: 16px 0;
}
</style>

3. 高级配置和技巧

3.1 共享依赖的高级配置

在 Module Federation 中,我们可以对共享依赖进行更精细的配置,如指定版本范围、是否为单例等。

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'federation_app',
      // ...
      shared: {
        vue: {
          requiredVersion: '^3.3.0', // 要求的 Vue 版本范围
          singleton: true, // 是否为单例,确保整个应用中只有一个 Vue 实例
          strictVersion: true, // 是否严格匹配版本
          version: '3.3.4' // 当前应用的 Vue 版本
        },
        pinia: {
          singleton: true,
          strictVersion: false
        }
      }
    })
  ]
  // ...
})

3.2 使用 Pinia 进行状态共享

除了使用组合式 API 创建简单的状态管理外,我们还可以使用 Pinia 进行更复杂的状态共享。

首先,在 Remote 应用中安装并配置 Pinia:

# 在 Remote 应用中安装 Pinia
npm install pinia
// federation-remote/src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

创建 Pinia store:

// federation-remote/src/stores/piniaStore.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Vue 3 + Pinia'
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    reset() {
      this.count = 0
    }
  }
})

在 Remote 应用的 vite.config.js 中暴露 Pinia store:

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'federation_remote',
      filename: 'remoteEntry.js',
      exposes: {
        // ...
        './piniaStore': './src/stores/piniaStore.js'
      },
      shared: ['vue', 'pinia'] // 共享 Pinia
    })
  ]
  // ...
})

在 Host 应用中安装并配置 Pinia:

# 在 Host 应用中安装 Pinia
npm install pinia
// federation-host/src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

在 Host 应用的 vite.config.js 中添加 Pinia 到共享依赖:

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'federation_host',
      remotes: {
        'federation_remote': 'http://localhost:5174/assets/remoteEntry.js'
      },
      shared: ['vue', 'pinia'] // 共享 Pinia
    })
  ]
  // ...
})

在 Host 应用中使用 Remote 应用的 Pinia store:

<!-- federation-host/src/components/PiniaStoreComponent.vue -->
<template>
  <div class="pinia-store">
    <h3>Pinia Store from Remote</h3>
    <p>Name: {{ counterStore.name }}</p>
    <p>Count: {{ counterStore.count }}</p>
    <p>Double Count: {{ counterStore.doubleCount }}</p>
    <div class="buttons">
      <button @click="counterStore.increment">Increment</button>
      <button @click="counterStore.decrement">Decrement</button>
      <button @click="counterStore.reset">Reset</button>
    </div>
  </div>
</template>

<script setup>
// 动态导入远程 Pinia store
const { useCounterStore } = await import('federation_remote/piniaStore')
const counterStore = useCounterStore()
</script>

<style scoped>
.pinia-store {
  background-color: #f0f0f0;
  padding: 16px;
  border-radius: 8px;
  margin: 16px 0;
}

.buttons {
  margin-top: 16px;
}

button {
  background-color: #42b983;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  margin-right: 8px;
}

button:hover {
  background-color: #369f71;
}
</style>

3.3 处理异步组件加载状态

当动态加载远程组件时,我们可以使用 Vue 3 的 Suspense 组件来处理加载状态。

<template>
  <div class="app">
    <h1>Vue 3 Module Federation Host App</h1>
    
    <h2>Remote Component with Loading State:</h2>
    <Suspense>
      <template #default>
        <RemoteComponent />
      </template>
      <template #fallback>
        <div class="loading">Loading Remote Component...</div>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
// 动态导入远程组件
const RemoteComponent = () => import('federation_remote/RemoteComponent')
</script>

<style scoped>
.loading {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
  font-size: 18px;
  color: #666;
}
</style>

3.4 多 Remote 应用集成

Module Federation 支持同时集成多个 Remote 应用,我们可以在 Host 应用中配置多个 Remote 应用。

// federation-host/vite.config.js
export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'federation_host',
      remotes: {
        'remote_app_1': 'http://localhost:5174/assets/remoteEntry.js',
        'remote_app_2': 'http://localhost:5175/assets/remoteEntry.js',
        'remote_app_3': 'http://localhost:5176/assets/remoteEntry.js'
      },
      shared: ['vue', 'pinia']
    })
  ]
  // ...
})

在 Host 应用中使用多个 Remote 应用的模块:

<template>
  <div class="app">
    <h1>Vue 3 Module Federation Host App</h1>
    
    <h2>Remote App 1 Component:</h2>
    <Suspense>
      <template #default>
        <RemoteComponent1 />
      </template>
      <template #fallback>
        <div class="loading">Loading Remote Component 1...</div>
      </template>
    </Suspense>
    
    <h2>Remote App 2 Component:</h2>
    <Suspense>
      <template #default>
        <RemoteComponent2 />
      </template>
      <template #fallback>
        <div class="loading">Loading Remote Component 2...</div>
      </template>
    </Suspense>
    
    <h2>Remote App 3 Component:</h2>
    <Suspense>
      <template #default>
        <RemoteComponent3 />
      </template>
      <template #fallback>
        <div class="loading">Loading Remote Component 3...</div>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
// 动态导入多个远程组件
const RemoteComponent1 = () => import('remote_app_1/RemoteComponent')
const RemoteComponent2 = () => import('remote_app_2/RemoteComponent')
const RemoteComponent3 = () => import('remote_app_3/RemoteComponent')
</script>

4. 构建和部署

4.1 构建 Remote 应用

# 在 Remote 应用目录下执行
npm run build

构建完成后,会生成 dist 目录,其中包含 remoteEntry.js 文件和其他构建产物。

4.2 构建 Host 应用

# 在 Host 应用目录下执行
npm run build

构建完成后,会生成 dist 目录,其中包含 Host 应用的构建产物。

4.3 部署考虑

在部署 Module Federation 应用时,需要注意以下几点:

  1. Static Assets:确保 Remote 应用的静态资源(包括 remoteEntry.js)可以通过正确的 URL 访问
  2. CORS:确保 Remote 应用的服务器配置了正确的 CORS 头,允许 Host 应用访问
  3. Versioning:考虑使用版本控制机制,确保 Host 应用加载的是正确版本的 Remote 模块
  4. Fallback Mechanism:实现适当的回退机制,当 Remote 模块加载失败时能够优雅处理
  5. CDN 考虑:可以将 Remote 应用的静态资源部署到 CDN 上,提高加载速度

最佳实践

1. 合理规划模块边界

  • 遵循单一职责原则,每个暴露的模块只负责一个功能
  • 保持暴露模块的 API 简洁明了,便于 Host 应用使用
  • 避免暴露过于复杂的模块,尽量将复杂性封装在 Remote 应用内部
  • 考虑模块的复用性,设计通用的组件和工具函数

2. 优化依赖共享

  • 只共享必要的依赖,避免共享过多依赖导致的性能问题
  • 为共享依赖设置合理的版本范围,平衡兼容性和性能
  • 对于频繁更新的依赖,考虑不共享,避免版本冲突
  • 使用 singleton: true 确保全局状态管理库(如 Pinia)在整个应用中只有一个实例

3. 处理加载状态和错误

  • 使用 Suspense 组件处理远程组件的加载状态
  • 实现适当的错误边界,处理远程模块加载失败的情况
  • 添加加载动画和错误提示,提高用户体验
  • 考虑实现重试机制,在网络不稳定的情况下自动重试加载

4. 确保类型安全

  • 如果使用 TypeScript,可以生成类型声明文件并暴露给 Host 应用
  • 在 Host 应用中使用类型声明,确保类型安全
  • 考虑使用 dts-bundle-generator 等工具生成类型声明文件

5. 测试策略

  • 分别测试 Host 和 Remote 应用的功能
  • 测试 Host 应用在 Remote 应用不可用情况下的行为
  • 测试不同版本的 Remote 应用对 Host 应用的影响
  • 考虑使用 Cypress 或 Playwright 进行端到端测试

6. 监控和日志

  • 添加监控代码,跟踪远程模块的加载时间和成功率
  • 实现日志记录,便于调试和分析问题
  • 考虑使用 Sentry 或 LogRocket 等工具进行应用监控

常见问题和解决方案

1. 依赖版本冲突

问题:Host 和 Remote 应用使用了不同版本的共享依赖,导致运行时错误。

解决方案

  • 使用 strictVersion: false 允许使用不同版本的依赖
  • 为共享依赖设置合理的版本范围
  • 在 Host 应用中指定兼容的依赖版本

2. 远程模块加载失败

问题:Host 应用无法加载 Remote 模块,提示 "Cannot read properties of undefined (reading 'get')" 或类似错误。

解决方案

  • 检查 Remote 应用的 remoteEntry.js 文件是否可以通过正确的 URL 访问
  • 检查 Remote 应用的 vite.config.jsexposes 配置是否正确
  • 检查 Host 应用的 vite.config.jsremotes 配置是否正确
  • 确保 Remote 应用的服务器配置了正确的 CORS 头

3. 共享状态不同步

问题:Host 和 Remote 应用使用的共享状态不同步。

解决方案

  • 确保共享状态管理库(如 Pinia)配置了 singleton: true
  • 避免在 Remote 应用中直接修改全局状态
  • 使用事件总线或其他通信机制确保状态同步

4. 构建产物过大

问题:构建产物过大,导致加载速度慢。

解决方案

  • 优化 Remote 应用的构建配置,减少打包体积
  • 使用代码分割,按需加载模块
  • 考虑将 Remote 应用的静态资源部署到 CDN 上
  • 只暴露必要的模块,避免暴露不必要的代码

5. 开发环境配置复杂

问题:在开发环境中需要同时启动多个应用,配置复杂。

解决方案

  • 使用 npm-run-all 等工具同时启动多个应用
  • 创建统一的开发脚本,简化开发流程
  • 考虑使用 Docker Compose 管理多个应用的开发环境

进阶学习资源

1. 官方文档

2. 教程和博客

3. 视频资源

4. 开源项目和示例

实践练习

练习 1:基础 Module Federation 集成

  1. 创建一个 Vue 3 Host 应用
  2. 创建一个 Vue 3 Remote 应用
  3. 配置 Remote 应用,暴露一个组件和一个工具函数
  4. 配置 Host 应用,引用 Remote 应用
  5. 在 Host 应用中使用 Remote 应用的组件和工具函数
  6. 测试应用的构建和运行

练习 2:使用 Pinia 进行状态共享

  1. 在 Remote 应用中安装并配置 Pinia
  2. 创建一个 Pinia store 用于计数功能
  3. 在 Remote 应用中暴露 Pinia store
  4. 在 Host 应用中安装并配置 Pinia
  5. 在 Host 应用中使用 Remote 应用的 Pinia store
  6. 测试状态共享功能

练习 3:处理加载状态和错误

  1. 在 Host 应用中使用 Suspense 组件处理远程组件的加载状态
  2. 实现错误边界,处理远程模块加载失败的情况
  3. 添加加载动画和错误提示
  4. 测试不同网络条件下的应用表现

练习 4:多 Remote 应用集成

  1. 创建两个 Remote 应用
  2. 在 Host 应用中配置对两个 Remote 应用的引用
  3. 在 Host 应用中使用两个 Remote 应用的组件
  4. 测试应用的构建和运行

练习 5:生产环境部署

  1. 构建 Remote 应用和 Host 应用
  2. 部署 Remote 应用到本地服务器
  3. 更新 Host 应用的配置,指向部署后的 Remote 应用
  4. 构建并部署 Host 应用
  5. 测试生产环境下的应用表现

总结

Module Federation 是 Webpack 5 引入的一项革命性特性,它为 Vue 3 应用提供了全新的模块共享方式。通过 Module Federation,我们可以构建更加灵活、可扩展的 Vue 3 应用,实现不同团队之间的独立开发和部署。

在本集中,我们学习了 Module Federation 的核心概念、工作原理以及如何在 Vue 3 应用中实现 Module Federation。我们探讨了如何创建联邦应用、共享组件和状态、处理依赖共享等高级技巧,并介绍了相关的最佳实践和常见问题解决方案。

Module Federation 为微前端架构提供了强大的支持,使得我们可以构建更加灵活、可扩展的大型应用。通过掌握 Module Federation,你将能够更好地应对现代 Web 应用开发中的挑战,构建出更高质量、更易于维护的 Vue 3 应用。

下一集我们将学习 Vue 3 与 PWA 进阶,敬请期待!

« 上一篇 Vue 3与微前端架构 - 构建可扩展大型前端应用的核心技术 下一篇 » Vue 3与PWA进阶 - 构建现代化离线应用的核心技术