BFF 层与微服务架构

当后端从单体演进到微服务,前端面临一个经典问题:一个页面需要从 3-5 个微服务聚合数据,直接从客户端调用会导致请求瀑布、数据过度获取、协议不一致。BFF(Backend for Frontend)层就是解决方案——Next.js 天然就是一个 BFF,它的 Server Component 和 Server Action 直接充当前端专属的服务编排层。

1. BFF 模式

1.1 什么是 BFF

没有 BFF:
  浏览器 → 用户服务(获取用户信息)
  浏览器 → 项目服务(获取项目列表)
  浏览器 → 计费服务(获取订阅状态)
  浏览器 → 通知服务(获取未读数)
  = 4 次请求,客户端聚合,慢

有 BFF:
  浏览器 → Next.js BFF → 用户服务
                        → 项目服务    } 并行调用
                        → 计费服务    } 服务端聚合
                        → 通知服务
  = 1 次请求(浏览器到 BFF),BFF 内部并行 4 次

1.2 Next.js 为什么是天然 BFF

传统 BFF:
  浏览器 → React SPA → BFF 服务器(Express/NestJS)→ 微服务
  需要单独维护一个 BFF 服务

Next.js BFF:
  浏览器 → Next.js(Server Component 直接调用微服务)→ 微服务
  BFF 逻辑就写在 Server Component / Server Action 里,不需要额外服务
BFF 能力Next.js 对应
数据聚合Server Component + Promise.all
数据裁剪只返回前端需要的字段
协议转换Server Action 内部调 gRPC/REST/GraphQL
认证传递middleware + cookies 透传
缓存unstable_cache / ISR
错误处理try-catch + 降级 UI

2. tRPC 全栈类型安全

2.1 为什么用 tRPC

REST API 的问题:
  前端:fetch('/api/projects')  → 响应类型是 any
  后端:app.get('/api/projects') → 没有类型约束
  
tRPC 的方案:
  前端:trpc.project.list.useQuery()  → 自动推导返回类型
  后端:router.project.list()          → 入参和返回类型一致
  
  从后端到前端,全链路类型安全,零代码生成。

2.2 tRPC + Next.js App Router

// lib/trpc/init.ts
import { initTRPC, TRPCError } from '@trpc/server'
import superjson from 'superjson'
import { type Context } from './context'
 
const t = initTRPC.context<Context>().create({
  transformer: superjson,
})
 
export const router = t.router
export const publicProcedure = t.procedure
 
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
  if (!ctx.userId) {
    throw new TRPCError({ code: 'UNAUTHORIZED' })
  }
  return next({ ctx: { ...ctx, userId: ctx.userId } })
})
// lib/trpc/context.ts
import { auth } from '@clerk/nextjs/server'
 
export async function createContext() {
  const { userId, orgId } = await auth()
  return { userId, tenantId: orgId }
}
 
export type Context = Awaited<ReturnType<typeof createContext>>

2.3 定义 Router

// lib/trpc/routers/project.ts
import { z } from 'zod'
import { router, protectedProcedure } from '../init'
import { db } from '@/lib/db'
import { projects } from '@/lib/db/schema'
 
export const projectRouter = router({
  list: protectedProcedure
    .input(z.object({
      page: z.number().min(1).default(1),
      limit: z.number().min(1).max(100).default(20),
    }))
    .query(async ({ ctx, input }) => {
      const data = await db
        .select()
        .from(projects)
        .where(eq(projects.tenantId, ctx.tenantId))
        .offset((input.page - 1) * input.limit)
        .limit(input.limit)
      return data
    }),
 
  create: protectedProcedure
    .input(z.object({
      name: z.string().min(1).max(100),
      description: z.string().max(500).optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      const [project] = await db
        .insert(projects)
        .values({ ...input, tenantId: ctx.tenantId })
        .returning()
      return project
    }),
 
  delete: protectedProcedure
    .input(z.object({ id: z.string().uuid() }))
    .mutation(async ({ ctx, input }) => {
      await db
        .delete(projects)
        .where(and(
          eq(projects.id, input.id),
          eq(projects.tenantId, ctx.tenantId),
        ))
    }),
})
// lib/trpc/routers/_app.ts
import { router } from '../init'
import { projectRouter } from './project'
import { userRouter } from './user'
import { billingRouter } from './billing'
 
export const appRouter = router({
  project: projectRouter,
  user: userRouter,
  billing: billingRouter,
})
 
export type AppRouter = typeof appRouter

2.4 Server-Side 调用(RSC)

// lib/trpc/server.ts
import 'server-only'
import { createTRPCContext } from './context'
import { appRouter } from './routers/_app'
import { createCallerFactory } from '@trpc/server'
 
const createCaller = createCallerFactory(appRouter)
 
export async function caller() {
  const ctx = await createTRPCContext()
  return createCaller(ctx)
}
// app/(dashboard)/projects/page.tsx
import { caller } from '@/lib/trpc/server'
 
export default async function ProjectsPage() {
  const trpc = await caller()
  const projects = await trpc.project.list({ page: 1, limit: 20 })
  
  return <ProjectList projects={projects} />
}

2.5 Client-Side 调用

// lib/trpc/client.ts
'use client'
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from './routers/_app'
 
export const trpc = createTRPCReact<AppRouter>()
// components/project-list-client.tsx
'use client'
import { trpc } from '@/lib/trpc/client'
 
export function ProjectListClient() {
  const { data, isLoading } = trpc.project.list.useQuery({ page: 1 })
  
  if (isLoading) return <Skeleton />
  return (
    <ul>
      {data?.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  )
}

3. 服务编排

3.1 并行聚合

// app/(dashboard)/page.tsx — Dashboard 页面聚合多个服务
export default async function DashboardPage() {
  const trpc = await caller()
  
  // 并行调用 4 个服务
  const [projects, stats, notifications, billing] = await Promise.all([
    trpc.project.list({ page: 1, limit: 5 }),
    trpc.analytics.overview(),
    trpc.notification.unreadCount(),
    trpc.billing.currentPlan(),
  ])
  
  return (
    <div className="grid grid-cols-2 gap-4">
      <RecentProjects projects={projects} />
      <StatsCard stats={stats} />
      <NotificationBell count={notifications} />
      <PlanBadge plan={billing.plan} />
    </div>
  )
}

3.2 瀑布模式(依赖关系)

// 有些调用依赖前一个结果
export default async function ProjectDetailPage({ params }: Props) {
  const { id } = await params
  const trpc = await caller()
  
  // 第一层:获取项目信息
  const project = await trpc.project.getById({ id })
  
  // 第二层:基于项目信息并行获取
  const [members, activity, files] = await Promise.all([
    trpc.member.listByProject({ projectId: project.id }),
    trpc.activity.recent({ projectId: project.id }),
    trpc.file.list({ projectId: project.id }),
  ])
  
  return <ProjectDetail project={project} members={members} activity={activity} files={files} />
}

3.3 数据裁剪

// 微服务返回完整的用户对象(20+ 字段)
// BFF 只返回前端需要的字段
 
export const dashboardRouter = router({
  overview: protectedProcedure.query(async ({ ctx }) => {
    const fullUser = await userService.getUser(ctx.userId)
    const fullProjects = await projectService.list(ctx.tenantId)
    
    // 裁剪:只返回前端需要的
    return {
      user: {
        name: fullUser.name,
        avatar: fullUser.avatarUrl,
        role: fullUser.role,
      },
      projects: fullProjects.map(p => ({
        id: p.id,
        name: p.name,
        updatedAt: p.updatedAt,
      })),
    }
  }),
})

4. 熔断降级

4.1 服务调用封装

// lib/services/base.ts
interface ServiceCallOptions {
  timeout?: number
  retries?: number
  fallback?: () => any
}
 
async function serviceCall<T>(
  fn: () => Promise<T>,
  options: ServiceCallOptions = {}
): Promise<T> {
  const { timeout = 5000, retries = 2, fallback } = options
  
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const controller = new AbortController()
      const timer = setTimeout(() => controller.abort(), timeout)
      
      const result = await fn()
      clearTimeout(timer)
      return result
    } catch (error) {
      if (attempt === retries) {
        // 所有重试失败,使用降级方案
        if (fallback) return fallback() as T
        throw error
      }
      // 指数退避
      await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 100))
    }
  }
  throw new Error('Unreachable')
}

4.2 降级策略

// app/(dashboard)/page.tsx
export default async function DashboardPage() {
  const trpc = await caller()
  
  const [projects, stats, notifications] = await Promise.allSettled([
    trpc.project.list({ page: 1 }),
    trpc.analytics.overview(),
    trpc.notification.unreadCount(),
  ])
  
  return (
    <div>
      {projects.status === 'fulfilled' 
        ? <ProjectList projects={projects.value} />
        : <ProjectListFallback />  // 降级:显示缓存数据或空状态
      }
      
      {stats.status === 'fulfilled'
        ? <StatsCard stats={stats.value} />
        : <StatsCardSkeleton />  // 降级:显示骨架屏
      }
      
      {notifications.status === 'fulfilled'
        ? <NotificationBell count={notifications.value} />
        : <NotificationBell count={0} />  // 降级:显示 0
      }
    </div>
  )
}

4.3 缓存作为降级

import { unstable_cache } from 'next/cache'
 
const getCachedStats = unstable_cache(
  async (tenantId: string) => {
    return analyticsService.overview(tenantId)
  },
  ['analytics-overview'],
  { revalidate: 300 }  // 5 分钟缓存
)
 
// 服务挂了 → 返回 5 分钟前的缓存数据
// 比显示错误好得多

5. 外部服务集成

5.1 REST 服务调用

// lib/services/payment.ts
class PaymentService {
  private baseUrl = process.env.PAYMENT_SERVICE_URL!
  
  async getSubscription(tenantId: string) {
    const res = await fetch(`${this.baseUrl}/subscriptions/${tenantId}`, {
      headers: {
        Authorization: `Bearer ${process.env.INTERNAL_API_KEY}`,
        'Content-Type': 'application/json',
      },
      next: { revalidate: 60, tags: ['subscription'] },
    })
    
    if (!res.ok) throw new Error(`Payment service error: ${res.status}`)
    return res.json()
  }
}
 
export const paymentService = new PaymentService()

5.2 gRPC 集成

// lib/services/grpc-client.ts
import { createChannel, createClient } from 'nice-grpc'
import { UserServiceDefinition } from './generated/user'
 
const channel = createChannel(process.env.USER_SERVICE_GRPC_URL!)
const userClient = createClient(UserServiceDefinition, channel)
 
export async function getUser(userId: string) {
  return userClient.getUser({ userId })
}
 
export async function listUsers(tenantId: string) {
  return userClient.listUsers({ tenantId })
}

5.3 多协议统一

// lib/services/index.ts — 统一服务层
import { paymentService } from './payment'    // REST
import { getUser } from './grpc-client'       // gRPC
import { analyticsClient } from './analytics' // GraphQL
 
// tRPC router 统一编排不同协议的服务
export const dashboardRouter = router({
  overview: protectedProcedure.query(async ({ ctx }) => {
    const [user, subscription, analytics] = await Promise.all([
      getUser(ctx.userId),                           // gRPC
      paymentService.getSubscription(ctx.tenantId),  // REST
      analyticsClient.query({ tenantId: ctx.tenantId }), // GraphQL
    ])
    
    return { user, subscription, analytics }
  }),
})

6. 架构决策

6.1 什么时候引入 BFF

单体后端 → 不需要 BFF,直接查数据库
2-3 个微服务 → Server Component 直接调用
5+ 个微服务 → 需要 BFF 层统一编排
跨团队服务 → BFF 是必须的

6.2 tRPC vs REST API Routes

场景推荐
前端内部调用tRPC(类型安全)
第三方 WebhookREST API Route
对外公开 APIREST + OpenAPI
移动端共用 APIREST 或 GraphQL

本章小结

  • Next.js 即 BFF:Server Component 和 Server Action 天然充当服务编排层
  • tRPC:全链路类型安全(后端 → 前端),零代码生成,RSC + Client 双模式调用
  • 服务编排:Promise.all 并行聚合、Promise.allSettled 容错、数据裁剪减少传输
  • 熔断降级:超时 + 重试 + 降级(缓存数据 / 骨架屏 / 默认值)
  • 多协议:REST / gRPC / GraphQL 在 tRPC router 中统一编排
  • 架构决策:5+ 微服务时引入 BFF,对内 tRPC、对外 REST