BFF 层与微服务架构

当后端拆分为多个微服务后,前端直接调用多个服务会面临跨域、认证传递、数据聚合、错误处理等问题。BFF(Backend for Frontend)层作为前端和微服务之间的中间层,负责请求路由、数据编排、协议转换和安全防护。Nuxt4 的 Nitro Server 天然适合做 BFF——它和前端在同一进程中,零网络延迟,还能利用 SSR 做服务端数据预取。

1. BFF 架构概述

1.1 为什么需要 BFF

没有 BFF:
┌──────────┐     ┌──────────────┐
│  前端     │────→│ 用户服务      │
│  (Nuxt)   │────→│ 视频服务      │
│           │────→│ 推荐服务      │
│           │────→│ 支付服务      │
└──────────┘     └──────────────┘
问题:多次请求、跨域、认证传递复杂

有 BFF:
┌──────────┐     ┌──────────┐     ┌──────────────┐
│  前端     │────→│  BFF     │────→│ 用户服务      │
│  (Nuxt)   │     │ (Nitro)  │────→│ 视频服务      │
│           │     │          │────→│ 推荐服务      │
└──────────┘     └──────────┘     └──────────────┘
优势:一次请求、统一认证、数据聚合

1.2 BFF 的核心职责

职责说明
请求聚合一次前端请求 → 并行调用多个微服务 → 合并返回
数据裁剪微服务返回完整数据,BFF 只返回前端需要的字段
协议转换前端 REST → BFF → 后端 gRPC/GraphQL
认证传递统一处理 JWT/Session → 转发给各微服务
缓存高频低变数据在 BFF 层缓存
熔断降级某个微服务挂了,返回降级数据

2. Nitro 作为 BFF

2.1 服务代理配置

// server/utils/services.ts
interface ServiceConfig {
  baseURL: string
  timeout: number
  headers?: Record<string, string>
}
 
const services: Record<string, ServiceConfig> = {
  user: {
    baseURL: process.env.USER_SERVICE_URL || 'http://user-service:3001',
    timeout: 5000,
  },
  video: {
    baseURL: process.env.VIDEO_SERVICE_URL || 'http://video-service:3002',
    timeout: 10000,
  },
  recommend: {
    baseURL: process.env.RECOMMEND_SERVICE_URL || 'http://recommend-service:3003',
    timeout: 3000,
  },
  payment: {
    baseURL: process.env.PAYMENT_SERVICE_URL || 'http://payment-service:3004',
    timeout: 15000,
  },
}
 
export function createServiceClient(name: keyof typeof services) {
  const config = services[name]
  if (!config) throw new Error(`Unknown service: ${name}`)
 
  return {
    async fetch<T>(path: string, options?: RequestInit & { timeout?: number }): Promise<T> {
      const controller = new AbortController()
      const timeout = setTimeout(
        () => controller.abort(),
        options?.timeout ?? config.timeout
      )
 
      try {
        const response = await $fetch<T>(`${config.baseURL}${path}`, {
          ...options,
          signal: controller.signal,
          headers: { ...config.headers, ...options?.headers },
        })
        return response
      } finally {
        clearTimeout(timeout)
      }
    },
  }
}

2.2 数据聚合 API

// server/api/bff/home.get.ts
export default defineEventHandler(async (event) => {
  const user = await getAuthUser(event)
  const userService = createServiceClient('user')
  const videoService = createServiceClient('video')
  const recommendService = createServiceClient('recommend')
 
  // 并行请求三个微服务
  const [profile, trending, recommended] = await Promise.allSettled([
    userService.fetch(`/users/${user.id}`),
    videoService.fetch('/videos/trending?limit=10'),
    recommendService.fetch(`/recommend/${user.id}?limit=20`),
  ])
 
  return {
    profile: profile.status === 'fulfilled' ? profile.value : null,
    trending: trending.status === 'fulfilled' ? trending.value : [],
    recommended: recommended.status === 'fulfilled' ? recommended.value : [],
  }
})

关键点Promise.allSettled 而非 Promise.all——某个服务挂了不影响其他数据返回。

2.3 认证传递中间件

// server/middleware/auth-proxy.ts
export default defineEventHandler(async (event) => {
  if (!event.path.startsWith('/api/bff/')) return
 
  const token = getCookie(event, 'session-token')
  if (!token) {
    throw createError({ statusCode: 401, message: 'Unauthorized' })
  }
 
  // 验证 token 并附加到 event context
  const session = await verifySessionToken(token)
  event.context.user = session.user
 
  // 设置内部服务调用的认证头
  event.context.serviceHeaders = {
    'X-Internal-Auth': process.env.INTERNAL_SERVICE_KEY!,
    'X-User-Id': session.user.id,
    'X-User-Role': session.user.role,
  }
})

3. 服务编排

3.1 串行依赖编排

// server/api/bff/video/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')!
  const videoService = createServiceClient('video')
  const userService = createServiceClient('user')
 
  // 第一步:获取视频详情
  const video = await videoService.fetch<Video>(`/videos/${id}`)
 
  // 第二步:基于视频的 authorId 获取作者信息(依赖上一步结果)
  const [author, relatedVideos] = await Promise.all([
    userService.fetch<User>(`/users/${video.authorId}`),
    videoService.fetch<Video[]>(`/videos/related/${id}?limit=8`),
  ])
 
  // 裁剪:只返回前端需要的字段
  return {
    video: {
      id: video.id,
      title: video.title,
      description: video.description,
      videoUrl: video.videoUrl,
      viewCount: video.viewCount,
      likeCount: video.likeCount,
      createdAt: video.createdAt,
    },
    author: {
      id: author.id,
      name: author.name,
      avatar: author.avatar,
      followerCount: author.followerCount,
    },
    relatedVideos: relatedVideos.map(v => ({
      id: v.id,
      title: v.title,
      thumbnail: v.thumbnail,
      duration: v.duration,
    })),
  }
})

3.2 请求批量合并

// server/utils/batcher.ts
export function createBatcher<K, V>(
  batchFn: (keys: K[]) => Promise<Map<K, V>>,
  options: { maxSize?: number; delayMs?: number } = {}
) {
  const { maxSize = 50, delayMs = 10 } = options
  let pending: Array<{ key: K; resolve: (v: V | undefined) => void }> = []
  let timer: NodeJS.Timeout | null = null
 
  async function flush() {
    const batch = pending.splice(0, maxSize)
    if (!batch.length) return
 
    const keys = batch.map(b => b.key)
    const results = await batchFn(keys)
 
    for (const item of batch) {
      item.resolve(results.get(item.key))
    }
  }
 
  return function load(key: K): Promise<V | undefined> {
    return new Promise((resolve) => {
      pending.push({ key, resolve })
      if (pending.length >= maxSize) {
        flush()
      } else if (!timer) {
        timer = setTimeout(() => {
          timer = null
          flush()
        }, delayMs)
      }
    })
  }
}
 
// 使用
const loadUser = createBatcher(async (ids: string[]) => {
  const users = await userService.fetch<User[]>(`/users/batch?ids=${ids.join(',')}`)
  return new Map(users.map(u => [u.id, u]))
})

4. 超时与熔断

4.1 超时处理

// server/utils/timeout.ts
export async function withTimeout<T>(
  promise: Promise<T>,
  ms: number,
  fallback?: T
): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  )
 
  try {
    return await Promise.race([promise, timeout])
  } catch (error) {
    if (fallback !== undefined) return fallback
    throw error
  }
}
 
// 使用
const trending = await withTimeout(
  videoService.fetch('/videos/trending'),
  3000,
  []  // 超时返回空数组
)

4.2 熔断器

// server/utils/circuit-breaker.ts
type BreakerState = 'closed' | 'open' | 'half-open'
 
export function createCircuitBreaker(options: {
  failureThreshold: number
  resetTimeout: number
  halfOpenRequests: number
}) {
  let state: BreakerState = 'closed'
  let failures = 0
  let lastFailure = 0
  let halfOpenAttempts = 0
 
  return async function execute<T>(fn: () => Promise<T>, fallback: T): Promise<T> {
    // 熔断器打开:直接返回降级数据
    if (state === 'open') {
      if (Date.now() - lastFailure > options.resetTimeout) {
        state = 'half-open'
        halfOpenAttempts = 0
      } else {
        return fallback
      }
    }
 
    // 半开状态:限制请求数量
    if (state === 'half-open' && halfOpenAttempts >= options.halfOpenRequests) {
      return fallback
    }
 
    try {
      if (state === 'half-open') halfOpenAttempts++
      const result = await fn()
 
      // 成功:重置状态
      failures = 0
      state = 'closed'
      return result
    } catch (error) {
      failures++
      lastFailure = Date.now()
 
      if (failures >= options.failureThreshold) {
        state = 'open'
        console.warn(`Circuit breaker opened: ${failures} failures`)
      }
 
      return fallback
    }
  }
}
 
// 使用
const recommendBreaker = createCircuitBreaker({
  failureThreshold: 5,    // 连续 5 次失败后熔断
  resetTimeout: 30000,    // 30 秒后尝试恢复
  halfOpenRequests: 2,    // 半开状态只允许 2 个请求
})
 
const recommended = await recommendBreaker(
  () => recommendService.fetch(`/recommend/${userId}`),
  []  // 降级:返回空推荐列表
)

4.3 重试策略

// server/utils/retry.ts
export async function withRetry<T>(
  fn: () => Promise<T>,
  options: { maxRetries?: number; delay?: number; backoff?: number } = {}
): Promise<T> {
  const { maxRetries = 3, delay = 1000, backoff = 2 } = options
 
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error) {
      if (attempt === maxRetries) throw error
      const waitTime = delay * Math.pow(backoff, attempt)
      await new Promise(resolve => setTimeout(resolve, waitTime))
    }
  }
 
  throw new Error('Unreachable')
}
 
// 使用:指数退避重试(1s → 2s → 4s)
const result = await withRetry(
  () => paymentService.fetch('/payments/create', { method: 'POST', body }),
  { maxRetries: 3, delay: 1000, backoff: 2 }
)

5. BFF 缓存策略

5.1 多级缓存

// server/api/bff/trending.get.ts
export default defineCachedEventHandler(async () => {
  const videoService = createServiceClient('video')
  const trending = await videoService.fetch<Video[]>('/videos/trending')
 
  // BFF 层对数据裁剪后缓存
  return trending.map(v => ({
    id: v.id,
    title: v.title,
    thumbnail: v.thumbnail,
    viewCount: v.viewCount,
  }))
}, {
  maxAge: 60 * 5,
  swr: true,
  name: 'bff-trending',
})

5.2 条件缓存

// server/api/bff/user/feed.get.ts
export default defineEventHandler(async (event) => {
  const user = event.context.user
  const cacheKey = `feed:${user.id}`
  const storage = useStorage('cache')
 
  // 先查缓存
  const cached = await storage.getItem<Feed>(cacheKey)
  if (cached) return cached
 
  // 缓存未命中,请求微服务
  const feed = await buildUserFeed(user.id)
  await storage.setItem(cacheKey, feed, { ttl: 120 })
 
  return feed
})

6. 健康检查与监控

// server/api/health.get.ts
export default defineEventHandler(async () => {
  const checks = await Promise.allSettled([
    createServiceClient('user').fetch('/health'),
    createServiceClient('video').fetch('/health'),
    createServiceClient('recommend').fetch('/health'),
  ])
 
  const services = ['user', 'video', 'recommend'].map((name, i) => ({
    name,
    status: checks[i].status === 'fulfilled' ? 'healthy' : 'unhealthy',
    error: checks[i].status === 'rejected' ? (checks[i] as PromiseRejectedResult).reason?.message : undefined,
  }))
 
  const allHealthy = services.every(s => s.status === 'healthy')
 
  return {
    status: allHealthy ? 'healthy' : 'degraded',
    services,
    timestamp: new Date().toISOString(),
  }
})

本章小结

  • BFF 定位:前端和微服务之间的中间层,负责聚合、裁剪、认证、缓存、熔断
  • Nitro 优势:与 Nuxt 前端同进程、零网络延迟、SSR 数据预取天然融合
  • 服务编排Promise.allSettled 并行请求、串行依赖分步处理、批量合并减少请求数
  • 超时熔断:withTimeout 超时降级、CircuitBreaker 熔断器(closed → open → half-open)、指数退避重试
  • 缓存:defineCachedEventHandler BFF 层缓存、useStorage 用户级缓存
  • 监控:/api/health 端点聚合检查所有微服务状态