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 的工作原理基于以下几个核心机制:
- Remote Entry 文件:Remote 应用构建时会生成一个
remoteEntry.js文件,该文件包含了 Remote 应用暴露的模块信息和加载逻辑 - 动态加载:Host 应用通过加载 Remote 应用的
remoteEntry.js文件,获取 Remote 应用暴露的模块信息 - 模块加载:当 Host 应用需要使用 Remote 应用的模块时,会通过 Webpack 的 Runtime 动态加载该模块
- 依赖共享: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 install2.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 -D2.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 应用时,需要注意以下几点:
- Static Assets:确保 Remote 应用的静态资源(包括
remoteEntry.js)可以通过正确的 URL 访问 - CORS:确保 Remote 应用的服务器配置了正确的 CORS 头,允许 Host 应用访问
- Versioning:考虑使用版本控制机制,确保 Host 应用加载的是正确版本的 Remote 模块
- Fallback Mechanism:实现适当的回退机制,当 Remote 模块加载失败时能够优雅处理
- 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.js中exposes配置是否正确 - 检查 Host 应用的
vite.config.js中remotes配置是否正确 - 确保 Remote 应用的服务器配置了正确的 CORS 头
3. 共享状态不同步
问题:Host 和 Remote 应用使用的共享状态不同步。
解决方案:
- 确保共享状态管理库(如 Pinia)配置了
singleton: true - 避免在 Remote 应用中直接修改全局状态
- 使用事件总线或其他通信机制确保状态同步
4. 构建产物过大
问题:构建产物过大,导致加载速度慢。
解决方案:
- 优化 Remote 应用的构建配置,减少打包体积
- 使用代码分割,按需加载模块
- 考虑将 Remote 应用的静态资源部署到 CDN 上
- 只暴露必要的模块,避免暴露不必要的代码
5. 开发环境配置复杂
问题:在开发环境中需要同时启动多个应用,配置复杂。
解决方案:
- 使用
npm-run-all等工具同时启动多个应用 - 创建统一的开发脚本,简化开发流程
- 考虑使用 Docker Compose 管理多个应用的开发环境
进阶学习资源
1. 官方文档
2. 教程和博客
- Module Federation with Vue 3 and Vite
- Building Micro-Frontends with Vue 3 and Module Federation
- Advanced Module Federation Patterns
3. 视频资源
- Vue 3 + Module Federation Tutorial
- Micro-Frontends with Webpack 5 Module Federation
- Vue 3 Full Stack with Module Federation
4. 开源项目和示例
- Module Federation Examples
- Vue 3 + Vite + Module Federation Example
- Micro-Frontend Demo with Vue 3 and Module Federation
实践练习
练习 1:基础 Module Federation 集成
- 创建一个 Vue 3 Host 应用
- 创建一个 Vue 3 Remote 应用
- 配置 Remote 应用,暴露一个组件和一个工具函数
- 配置 Host 应用,引用 Remote 应用
- 在 Host 应用中使用 Remote 应用的组件和工具函数
- 测试应用的构建和运行
练习 2:使用 Pinia 进行状态共享
- 在 Remote 应用中安装并配置 Pinia
- 创建一个 Pinia store 用于计数功能
- 在 Remote 应用中暴露 Pinia store
- 在 Host 应用中安装并配置 Pinia
- 在 Host 应用中使用 Remote 应用的 Pinia store
- 测试状态共享功能
练习 3:处理加载状态和错误
- 在 Host 应用中使用
Suspense组件处理远程组件的加载状态 - 实现错误边界,处理远程模块加载失败的情况
- 添加加载动画和错误提示
- 测试不同网络条件下的应用表现
练习 4:多 Remote 应用集成
- 创建两个 Remote 应用
- 在 Host 应用中配置对两个 Remote 应用的引用
- 在 Host 应用中使用两个 Remote 应用的组件
- 测试应用的构建和运行
练习 5:生产环境部署
- 构建 Remote 应用和 Host 应用
- 部署 Remote 应用到本地服务器
- 更新 Host 应用的配置,指向部署后的 Remote 应用
- 构建并部署 Host 应用
- 测试生产环境下的应用表现
总结
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 进阶,敬请期待!