微前端与模块联邦
当 Nuxt 应用膨胀到几十个页面、多个团队协作时,单体应用会遇到构建慢、部署互相阻塞、技术栈锁定等问题。微前端将单体拆分为多个可独立开发、独立部署的子应用。本章对比 Nuxt 生态中的几种微前端方案——Nuxt Layers、Module Federation、iframe 集成和 Web Components——分析各自适用场景,并给出完整的 Module Federation 集成实战。
1. 为什么需要微前端
1.1 单体应用的瓶颈
| 问题 | 表现 |
|---|---|
| 构建时间 | 单体项目 > 100 页面后,nuxi build 超过 5 分钟 |
| 部署耦合 | 修改一个模块必须全量发布 |
| 团队冲突 | 多团队在同一仓库频繁 merge conflict |
| 技术锁定 | 无法渐进式引入新框架或升级 |
1.2 微前端的拆分维度
按业务域拆分:
├── 主应用(Shell) → 导航、认证、布局
├── 视频子应用 → /video/**(视频团队维护)
├── 创作者子应用 → /creator/**(创作者团队维护)
├── 管理后台子应用 → /admin/**(平台团队维护)
└── 公共组件库 → 共享的 UI 组件和工具函数
2. 方案对比
2.1 四种方案总览
| 方案 | SSR 支持 | 独立部署 | 共享依赖 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| Nuxt Layers | ✅ | ❌ | ✅ | 低 | 同技术栈、monorepo |
| Module Federation | ⚠️ | ✅ | ✅ | 高 | 独立团队、独立部署 |
| iframe | ✅ | ✅ | ❌ | 低 | 异构技术栈、强隔离 |
| Web Components | ✅ | ✅ | ❌ | 中 | 跨框架组件共享 |
2.2 Nuxt Layers(推荐起步方案)
Nuxt Layers 是官方支持的"轻量微前端"——每个 Layer 是一个独立的 Nuxt 项目,可以提供组件、composables、页面、server 路由。
monorepo/
├── apps/
│ └── shell/ # 主应用
│ └── nuxt.config.ts
├── layers/
│ ├── video/ # 视频 Layer
│ │ ├── components/
│ │ ├── composables/
│ │ ├── pages/
│ │ ├── server/
│ │ └── nuxt.config.ts
│ ├── creator/ # 创作者 Layer
│ └── shared/ # 公共 Layer
└── pnpm-workspace.yaml
// apps/shell/nuxt.config.ts
export default defineNuxtConfig({
extends: [
'../../layers/shared',
'../../layers/video',
'../../layers/creator',
],
})优点:零额外依赖、完美 SSR、TypeScript 类型共享、HMR 正常工作。
缺点:构建还是一体的,不能独立部署。
2.3 何时选择 Module Federation
当你需要独立部署——每个子应用有自己的 CI/CD、可以独立发版、运行时动态加载——才需要 Module Federation。
3. Module Federation 集成
3.1 架构设计
┌─────────────────────────────────────────┐
│ Shell(主应用) │
│ ┌───────────┐ ┌───────────┐ │
│ │ 导航 / 布局│ │ 认证 / 权限│ │
│ └───────────┘ └───────────┘ │
│ │
│ ┌─── Remote ───┐ ┌─── Remote ───┐ │
│ │ Video App │ │ Admin App │ │
│ │ (独立部署) │ │ (独立部署) │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ┌─── Shared ────────────────────┐ │
│ │ Vue, Pinia, Nuxt UI (共享) │ │
│ └────────────────────────────────┘ │
└─────────────────────────────────────────┘
3.2 Vite Module Federation 配置
pnpm add @originjs/vite-plugin-federation -DRemote 应用(视频子应用):
// apps/video/vite.config.ts
import federation from '@originjs/vite-plugin-federation'
export default defineConfig({
plugins: [
federation({
name: 'videoApp',
filename: 'remoteEntry.js',
exposes: {
'./VideoList': './src/components/VideoList.vue',
'./VideoPlayer': './src/components/VideoPlayer.vue',
'./useVideo': './src/composables/useVideo.ts',
},
shared: ['vue', 'pinia'],
}),
],
build: {
target: 'esnext',
minify: false,
cssCodeSplit: false,
},
})Host 应用(主应用):
// apps/shell/vite.config.ts
import federation from '@originjs/vite-plugin-federation'
export default defineConfig({
plugins: [
federation({
name: 'shellApp',
remotes: {
videoApp: 'http://localhost:5001/assets/remoteEntry.js',
},
shared: ['vue', 'pinia'],
}),
],
})3.3 在 Nuxt 中集成
// nuxt.config.ts — 通过 Vite 插件集成
import federation from '@originjs/vite-plugin-federation'
export default defineNuxtConfig({
vite: {
plugins: [
federation({
name: 'shellApp',
remotes: {
videoApp: {
external: `Promise.resolve(window.__remotes__?.videoApp || '${process.env.VIDEO_APP_URL}/assets/remoteEntry.js')`,
externalType: 'promise',
},
},
shared: ['vue'],
}),
],
},
})3.4 动态加载远程组件
<!-- pages/video/index.vue -->
<script setup lang="ts">
const VideoList = defineAsyncComponent(
() => import('videoApp/VideoList')
)
</script>
<template>
<div>
<h1>视频列表</h1>
<Suspense>
<VideoList />
<template #fallback>
<div class="animate-pulse h-96 bg-gray-100 rounded" />
</template>
</Suspense>
</div>
</template>3.5 SSR 注意事项
Module Federation 目前对 SSR 的支持有限。推荐策略:
// 远程组件仅在客户端加载
const VideoList = import.meta.client
? defineAsyncComponent(() => import('videoApp/VideoList'))
: () => h('div', { class: 'skeleton' })或使用 .client.vue 后缀封装远程组件。
4. iframe 集成方案
4.1 适用场景
- 需要强隔离(CSS、JS 完全隔离)
- 集成异构技术栈(React + Vue + Angular)
- 第三方系统嵌入
4.2 iframe 通信
<!-- 主应用 -->
<script setup lang="ts">
const iframeRef = useTemplateRef<HTMLIFrameElement>('iframe')
function sendToChild(data: any) {
iframeRef.value?.contentWindow?.postMessage(
{ type: 'SHELL_EVENT', payload: data },
'https://video.example.com'
)
}
onMounted(() => {
window.addEventListener('message', (event) => {
if (event.origin !== 'https://video.example.com') return
if (event.data.type === 'CHILD_EVENT') {
handleChildEvent(event.data.payload)
}
})
})
</script>
<template>
<iframe
ref="iframe"
src="https://video.example.com/embed"
class="w-full h-[calc(100vh-64px)] border-0"
sandbox="allow-scripts allow-same-origin allow-forms"
/>
</template>4.3 iframe 的局限
- SEO:iframe 内容不被搜索引擎索引
- 性能:每个 iframe 是独立的浏览器上下文
- UX:滚动、弹窗、路由同步需要额外处理
5. Web Components 方案
5.1 Vue 组件导出为 Web Component
// video-player.ce.ts
import { defineCustomElement } from 'vue'
import VideoPlayer from './VideoPlayer.vue'
const VideoPlayerElement = defineCustomElement(VideoPlayer)
customElements.define('video-player', VideoPlayerElement)5.2 在 Nuxt 中使用 Web Components
// nuxt.config.ts
export default defineNuxtConfig({
vue: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('video-'),
},
},
})<template>
<video-player :src="videoUrl" @ended="handleEnd" />
</template>适用场景:把 Vue 组件分享给 React/Angular 项目使用。
6. 独立部署与版本管理
6.1 子应用独立 CI/CD
# .github/workflows/deploy-video-app.yml
name: Deploy Video App
on:
push:
paths: ['apps/video/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pnpm --filter video-app build
- run: pnpm --filter video-app deploy:cdn
env:
CDN_BUCKET: ${{ secrets.CDN_BUCKET }}6.2 版本发现与回滚
// 主应用动态发现远程入口
async function getRemoteEntry(appName: string): Promise<string> {
const manifest = await $fetch<Record<string, string>>('/api/micro-frontend/manifest')
return manifest[appName]
// 返回如 "https://cdn.example.com/video-app/v2.3.1/remoteEntry.js"
}// server/api/micro-frontend/manifest.get.ts
export default defineCachedEventHandler(async () => {
return {
videoApp: process.env.VIDEO_APP_REMOTE_ENTRY || 'http://localhost:5001/assets/remoteEntry.js',
adminApp: process.env.ADMIN_APP_REMOTE_ENTRY || 'http://localhost:5002/assets/remoteEntry.js',
}
}, { maxAge: 60 })本章小结
- Nuxt Layers(推荐起步):零依赖、完美 SSR、适合 monorepo 同技术栈拆分
- Module Federation:独立部署、运行时加载、共享依赖——适合多团队独立交付,但 SSR 支持有限
- iframe:强隔离、异构技术栈——SEO 和 UX 有代价
- Web Components:跨框架组件共享——适合向外输出 UI 组件
- 选型建议:先用 Layers 拆分,当团队规模和部署需求增长到必须独立部署时再引入 Module Federation