BFF 容错、超时与降级设计
Promise.allSettled 聚合、熔断、超时链、错误码规范与 partial response 的前端协作模式。
BFF 在微服务中的定位、Koa 中间件模型、API 聚合、鉴权与 Node 服务工程化实践。
Dashboard 页需要 user + 统计 + 公告三个微服务,直连时前端 3 个 RTT + 3 套错误格式,移动端弱网首屏 2.8s。加 BFF 聚合后 1 个 RTT、统一 { code, data },P75 1.1s。BFF 不是「前端写后端」,而是 按 UI 形态裁剪 API。
Web / App / 小程序
↓
BFF (Node/Koa) ← 聚合、裁剪、鉴权、缓存
↓
用户 / 订单 / 商品 微服务
与 API Gateway 分工:Gateway 管 TLS、限流、路由;BFF 管 页面级聚合。不要重复鉴权逻辑两处各写一套。
router.get('/api/v1/dashboard', auth, async (ctx) => {
const userId = ctx.state.userId;
const results = await Promise.allSettled([
withTimeout(userService.profile(userId), 2000),
withTimeout(statsService.summary(userId), 2000),
withTimeout(noticeService.latest(5), 2000),
]);
ctx.body = {
code: 0,
data: {
user: unwrap(results[0], null),
stats: unwrap(results[1], { empty: true }),
notices: unwrap(results[2], []),
},
partial: results.some((r) => r.status === 'rejected'),
};
});
partial: true 时前端展示降级 UI,比 500 整页错误体验好。
import CircuitBreaker from 'opossum';
const breaker = new CircuitBreaker(orderService.getDetail, {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000,
});
breaker.fallback(() => ({ degraded: true }));
// 短 TTL 缓存 — 公告类
const cache = new Map<string, { exp: number; data: unknown }>();
async function cachedNotices(key: string, fn: () => Promise<unknown>) {
const hit = cache.get(key);
if (hit && hit.exp > Date.now()) return hit.data;
const data = await fn();
cache.set(key, { data, exp: Date.now() + 60_000 });
return data;
}
async function auth(ctx: Context, next: Next) {
const token = ctx.headers.authorization?.replace(/^Bearer /, '');
if (!token) throw httpError(401);
const payload = await verifyJwt(token);
ctx.state.userId = payload.sub;
ctx.state.permissions = payload.permissions; // 供下游裁剪
await next();
}
字段裁剪示例:App 端 GET /profile 不返回 internalNotes;Web 端返回。BFF 层 pick(user, FIELDS_BY_CLIENT[ctx.state.client])。
| REST BFF | GraphQL | |
|---|---|---|
| 按页聚合 | 天然匹配 | 需 schema 治理 |
| 缓存 | HTTP/CDN 友好 | 复杂 |
| 团队学习成本 | 低 | 高 |
| 我们选型 | REST BFF | 仅 Admin 探索 |
Rejected GraphQL:移动端场景固定,REST 一页一接口更清晰;GraphQL N+1 要 DataLoader,运维成本高。
// 结构化日志 + trace id
app.use(async (ctx, next) => {
const traceId = ctx.get('x-trace-id') || randomUUID();
ctx.state.traceId = traceId;
const start = Date.now();
await next();
logger.info({ traceId, path: ctx.path, ms: Date.now() - start, status: ctx.status });
});
同一域不同 client header:
const client = ctx.get('x-client'); // web | ios | android | weapp
const fields = FIELD_MATRIX[client] ?? FIELD_MATRIX.web;
小程序包体积敏感,列表接口字段比 Web 少 40%。
BFF 的成功标准是 前端不再为拼接口写 200 行 useEffect。聚合、降级、裁剪在一层做完,微服务保持领域纯粹。