微前端与模块联邦

当 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 -D

Remote 应用(视频子应用)

// 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