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 端点聚合检查所有微服务状态