主题
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
tsx
// 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 } })
})tsx
// 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
tsx
// 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),
))
}),
})tsx
// 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 appRouter2.4 Server-Side 调用(RSC)
tsx
// 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)
}tsx
// 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 调用
tsx
// lib/trpc/client.ts
'use client'
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from './routers/_app'
export const trpc = createTRPCReact<AppRouter>()tsx
// 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 并行聚合
tsx
// 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 瀑布模式(依赖关系)
tsx
// 有些调用依赖前一个结果
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 数据裁剪
tsx
// 微服务返回完整的用户对象(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 服务调用封装
tsx
// 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 降级策略
tsx
// 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 缓存作为降级
tsx
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 服务调用
tsx
// 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 集成
tsx
// 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 多协议统一
tsx
// 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(类型安全) |
| 第三方 Webhook | REST API Route |
| 对外公开 API | REST + OpenAPI |
| 移动端共用 API | REST 或 GraphQL |
本章小结
- Next.js 即 BFF:Server Component 和 Server Action 天然充当服务编排层
- tRPC:全链路类型安全(后端 → 前端),零代码生成,RSC + Client 双模式调用
- 服务编排:Promise.all 并行聚合、Promise.allSettled 容错、数据裁剪减少传输
- 熔断降级:超时 + 重试 + 降级(缓存数据 / 骨架屏 / 默认值)
- 多协议:REST / gRPC / GraphQL 在 tRPC router 中统一编排
- 架构决策:5+ 微服务时引入 BFF,对内 tRPC、对外 REST