Vue 3 微前端终极指南:从共享组件到路由、工具函数的全方位实战 🚀
摘要:还在为跨项目共享组件而烦恼吗?每次组件更新都要发版 npm,再到各个项目里更新依赖,简直是“沟通两小时,发布一下午”。今天,我们来聊聊微前端的利器——模块联邦(Module Federation),带你从共享一个简单的按钮开始,逐步深入到共享通用的工具函数,最后挑战最实用的场景——共享整个带路由的页面!
🤔 前言:我们遇到了什么问题?
在现代前端开发中,组件化思想已深入人心。但当我们的应用变得越来越庞大,甚至拆分成多个独立项目时,一个新的问题浮出水面:
- 组件复用难:一个设计精美的 Button 或一个功能复杂的 DataGrid 组件,如何在多个项目中复用?
传统方式的痛点:
- 复制粘贴:最原始的方式,但也是最糟糕的。一旦组件有 Bug 或需求变更,你需要去每个项目里都改一遍,简直是噩梦。
- 发布为 NPM 包:这是标准的解决方案。但它也带来了流程上的繁琐:修改组件 -> 打包 -> 发布 npm -> 其他项目更新依赖 -> 重新部署。一个小小的文案修改,可能都要走完这一整套流程,效率极低。
有没有一种方式,可以让 A 项目的模块,直接、实时地被 B 项目使用,就像使用内部模块一样简单?
答案是肯定的!模块联邦 (Module Federation) 就是为此而生的。
✨ 什么是模块联邦?
模块联邦是 Webpack 5 提出的一个革命性功能(现在 Vite 也有了强大的社区插件支持),它允许一个 JavaScript 应用在运行时动态地加载另一个应用的代码。
听起来有点抽象?我们把它具象化:
- Remote (提供方) :一个独立的应用,它将自己的某些模块(比如组件、函数、页面)“暴露”出去,供其他应用使用。
- Host (消费方) :另一个独立的应用,它可以“引用”并渲染 Remote 应用暴露出来的模块。
这种方式的最大优势在于:Remote 应用的更新可以无需 Host 应用重新部署,只要 Remote 重新部署,Host 刷新页面就能获取到最新的功能!
话不多说,让我们撸起袖子,直接开干!
场景设定与目录结构
附 Gitee 仓库地址: Module Federation: vite+vue 远程模块联邦技术,组件共享 ,建议下载代码查看
-
packages-center(Remote - 提供方)- 职责:存放和暴露公共模块,包括 UI 组件、工具函数和完整的路由页面。
- 运行在:
http://localhost:3001
-
main-app(Host - 消费方)- 职责:主应用,负责整体布局和路由,并消费
packages-center提供的各种模块。 - 运行在:
http://localhost:3000
- 职责:主应用,负责整体布局和路由,并消费
项目目录结构概览:
清晰的目录结构有助于我们理解模块的组织方式。
packages-center (提供方) 目录结构:
Bash
packages-center/
├── src/
│ ├── components/
│ │ └── CoolButton.vue # 共享的 UI 组件
│ ├── utils/
│ │ └── format.js # 共享的工具函数
│ ├── view/
│ │ ├── About.vue # 共享的关于页面
│ │ ├── Home.vue # 共享的主页面
│ │ └── Share.vue # 共享的分享页面
│ ├── router/
│ │ └── index.js # 应用自身的路由定义
│ └── App.vue # 应用主入口
├── vite.config.js # 核心联邦配置
└── package.json
main-app (消费方) 目录结构:
Bash
main-app/
├── src/
│ ├── view/
│ │ └── main.vue # 应用自己的主页面,用于消费远程模块
│ ├── router/
│ │ └── index.js # 核心路由配置,负责集成远程页面
│ └── App.vue # 应用主入口,包含 <router-view>
├── vite.config.js # 核心联邦配置
└── package.json
🚀 实战演练:Vite 篇 (推荐)
Vite 凭借其极速的开发体验,在 Vue 3 社区中备受欢迎。我们可以使用 @originjs/vite-plugin-federation 插件来轻松实现模块联邦。
第一步:配置提供方 (packages-center)
1. 安装插件
Bash
# 进入 packages-center 项目目录 vite7.0以下版本
npm install @originjs/vite-plugin-federation --save-dev
#vite7.0以上版本
pnpm add @module-federation/vite -D
2. 准备好要共享的各类模块
根据上面的目录结构,确保 components、utils 和 view 目录下已经创建好了相应的模块文件。
3. 修改 vite.config.js
这是最核心的配置!我们需要告诉 Vite 要暴露哪些模块。
JavaScript
// packages-center/vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'
//版本不兼容 使用 @module-federation/vite
//import { federation } from '@module-federation/vite'
import path from 'path'
export default defineConfig({
server: {
port: 3001,
},
plugins: [
vue(),
federation({
// 模块联邦的全局唯一名称
name: 'packages-center',
// 导出的入口清单文件
filename: 'remoteEntry.js',
// exposes: 定义需要暴露的模块
exposes: {
// 1. 暴露 UI 组件
'./CoolButton': './src/components/CoolButton.vue',
// 2. 暴露工具函数
'./formatters': './src/utils/format.js',
// 3. 暴露路由页面
'./ShareView': './src/view/Share.vue',
'./AboutView': './src/view/About.vue',
'./HomeView': './src/view/Home.vue'
},
// shared: 定义需要与 Host 共享的依赖
shared: ['vue', 'vue-router']
})
],
// ... build 和 resolve 配置
})
OK!此时,packages-center 已经准备就绪,可以向外提供服务了。
第二步:配置消费方 (main-app)
现在,我们来 main-app 项目里接收并使用这些丰富多样的模块。
1. 安装插件 (同上)
Bash
# 进入 main-app 项目目录 vite7.0以下版本
npm install @originjs/vite-plugin-federation --save-dev
#vite7.0以上版本
pnpm add @module-federation/vite -D
2. 修改 vite.config.js
JavaScript
// main-app/vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'
//版本不兼容 使用 @module-federation/vite
//import { federation } from '@module-federation/vite'
import path from 'path'
export default defineConfig({
server: {
port: 3000
},
plugins: [
vue(),
federation({
name: 'main-app',
// remotes: 声明需要引用的远程模块
remotes: {
// 'packages_center' 是我们在本地使用的别名
// 'http://localhost:3001/dist/assets/remoteEntry.js' 是远程模块的地址
packages_center: 'http://localhost:3001/dist/assets/remoteEntry.js'
},
// 共享的依赖,应该和 remote 方保持一致
shared: ['vue', 'vue-router'],
})
],
// ... resolve 配置
})
📌 注意:这里的
remoteEntry.js地址指向了dist/assets目录。这意味着packages-center项目需要先执行npm run build进行打包,然后通过npm run dev或者vite preview这样的命令来提供dist目录的访问。
第三步:在 main-app 中消费远程模块
1. 基础篇:消费远程组件和工具函数
在 main-app/src/view/main.vue 页面中,我们同时使用了远程按钮和远程工具函数。
代码段
// main-app/src/view/main.vue
<template>
<div class="section">
<h2 class="section-title">远程组件</h2>
<Suspense>
<template #default>
<RemoteCoolButton @click="handleRemoteClick" />
</template>
<template #fallback>
<div>正在努力加载远程按钮...</div>
</template>
</Suspense>
</div>
<div class="section">
<h2 class="section-title">共享工具函数</h2>
<div>格式化后的金额:{{ money }}</div>
<div>格式化后的日期:{{ date }}</div>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
// 1. 异步加载远程组件
const RemoteCoolButton = defineAsyncComponent(() => import('packages_center/CoolButton'));
// 2. 直接导入远程工具函数
import { formatCurrency, formatDate } from 'packages_center/formatters';
const money = formatCurrency(123456.78);
const date = formatDate(new Date());
const handleRemoteClick = () => {
alert('你点击了来自 `packages-center` 的按钮!');
}
</script>
- 消费组件:我们使用
defineAsyncComponent异步加载,并用<Suspense>处理加载状态,这是加载远程UI的最佳方式。 - 消费工具函数:对于非UI类的纯JS模块,我们可以直接
import使用,就像使用本地模块一样简单!
2. 终极篇:消费并集成远程路由页面
这是模块联邦最强大的应用之一。主应用 main-app 可以将 packages-center 的整个页面无缝集成到自己的路由体系中。
main-app 的路由配置如下:
JavaScript
// main-app/src/router/index.js
import { createWebHistory, createRouter } from 'vue-router'
import { defineAsyncComponent } from 'vue'
const main = () => import('@/view/main.vue')
// 异步加载远程路由页面
const ShareView = defineAsyncComponent(() =>
import('packages_center/ShareView')
);
const ShareAboutView = defineAsyncComponent(() =>
import('packages_center/AboutView')
);
const ShareHomeView = defineAsyncComponent(() =>
import('packages_center/HomeView')
);
const routes = [
{ path: '/', component: main },
// 将主应用的路由路径,映射到远程应用的页面组件
{ path: '/shared', component: ShareView },
{ path: '/about', component: ShareAboutView },
{ path: '/share', component: ShareView },
{ path: '/home', component: ShareHomeView },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
通过这种配置,当用户访问 main-app 的 /shared、/about、/home 路径时,实际展示的是由 packages-center 提供的页面。更酷的是,在这些远程页面内部的路由跳转(例如在 Share.vue 中点击按钮执行 router.push('/about')),也会被 main-app 的路由系统接管,实现无缝的跨应用导航体验!
当然也可以通过 module 方式引入
{ path: '/home', component: () => import('packages_center/HomeView')},
那么 vite.config.js 需要改成
federation({
name: env.VITE_APP_PROJECT_NAME,
filename: 'remoteEntry.js',
remotes: {
socialSecurity: {
type: 'module',
name: 'packages_center',
entry: env.VITE_APP_APP_BASE_URL + '/cephr-web-socialSecurity/remoteEntry.js',
}
},
shared: ['vue', 'vue-router']
})
env.VITE_APP_APP_BASE_URL 为域名。
第四步:运行与验证
-
打包并预览
packages-centerBash
# 进入 packages-center 目录 npm run build npm run dev # 或者 vite preview,确保 dist 目录能被访问 -
启动
main-appBash
# 进入 main-app 目录 npm run dev
现在,访问 http://localhost:3000,你就能看到一个功能丰富的页面,它聚合了来自 packages-center 的组件、工具函数和可导航的页面,而这一切对于用户来说是完全无感的!
📌 核心要点与避坑指南
shared是关键:务必共享vue、vue-router、pinia等核心库。否则,每个应用都会打包一份,不仅增大体积,还可能因为实例不唯一导致各种奇怪的 Bug(例如router.push失效)。- 异步加载与
Suspense:远程组件加载需要网络请求,使用defineAsyncComponent和<Suspense>能极大提升用户体验,避免页面白屏。 - 远程模块地址:注意
remotes中remoteEntry.js的地址。开发环境和生产环境可能不同。生产环境通常指向CDN地址。 - CSS 样式隔离:在编写共享组件时,尽量使用
scopedCSS 来避免样式污染。 - 版本控制:虽然可以实时更新,但也带来了版本不一致的风险。重要更新建议做好通知,避免 breaking change 影响消费方应用。
- 如果报错
str is not iterable可能是由于vite版本不兼容,vite@7.*.*以上的版本请使用官方维护的pnpm add @module-federation/vite -Dwww.npmjs.com/package/@mo…
总结
模块联邦为前端微服务化提供了一种极其优雅和高效的实现方案。通过本文的实战,相信你已经掌握了如何在 Vue 3 项目中利用它来共享组件、工具函数和路由页面。
告别繁琐的 npm 发布流程,拥抱更加动态和灵活的架构吧!这不仅仅是组件复用,更是应用功能的自由组合与无缝集成。