BFF 层架构设计:从前端视角掌控 API
BFF 在微服务中的定位、Koa 中间件模型、API 聚合、鉴权与 Node 服务工程化实践。
Promise.allSettled 聚合、熔断、超时链、错误码规范与 partial response 的前端协作模式。
前置阅读:BFF 层架构设计 · 本专题第 2 篇
BFF 聚合 3 个下游时,任意一个超时不能拖垮整页。这篇讲我们在 Node BFF 里用的超时、降级与错误契约——前端能据此做 skeleton / 局部重试,而不是白屏。
客户端
↓ 4xx/5xx + { code, message, traceId }
BFF
↓ 下游超时 / 熔断 / 业务错误
微服务 A / B / C
| 层级 | 谁处理 | 前端表现 |
|---|---|---|
| 下游不可用 | BFF 降级 | 模块占位 + 「部分数据加载失败」 |
| 鉴权失败 | BFF 401 | 跳登录 |
| 参数错误 | BFF 400 | 表单 inline 错误 |
| 未知异常 | BFF 500 + traceId | 全局 toast + 反馈入口 |
原则:BFF 对外只暴露 一种 JSON 形态,不要把下游原始 stack 透传给浏览器。
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
return Promise.race([
p,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`timeout:${label}:${ms}`)), ms),
),
]);
}
聚合接口建议:单下游 2s、整接口 3s。整接口用 AbortController 在超时后 cancel 未完成的 fetch,避免僵尸请求占连接。
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 3000);
try {
const data = await fetchDashboard(userId, { signal: ac.signal });
ctx.body = { code: 0, data };
} finally {
clearTimeout(timer);
}
{
"code": 0,
"data": {
"user": { "name": "..." },
"stats": null,
"notices": []
},
"partial": true,
"errors": [
{ "module": "stats", "code": "DOWNSTREAM_TIMEOUT" }
],
"traceId": "tr_abc123"
}
前端约定:
if (data.partial) {
showBanner('部分模块加载失败,可点击重试');
}
if (data.errors?.some((e) => e.module === 'stats')) {
setStatsState('error');
}
不要 partial 时还当 success 全量渲染——用户会以为 stats 真的是空的。
下游连续失败时,BFF 侧短路(circuit breaker),直接返回缓存或空数据,保护线程池。
| 策略 | 适用 | 注意 |
|---|---|---|
| 快速失败 | 读多写少 Dashboard | 需缓存兜底 |
| 有限重试 | 幂等 GET | 最多 2 次,指数退避 |
| 不重试 | POST 支付 | 交给业务层 idempotency key |
重试只在 BFF → 下游 做,不要让浏览器对 BFF 自动重试 POST。
每个请求生成 traceId,贯穿 BFF 日志与下游 X-Request-Id。前端报错时把 traceId 带给客服/监控。
ctx.set('X-Trace-Id', traceId);
logger.info({ traceId, userId, path: ctx.path, partial: body.partial });
allSettled + unwrapcode 枚举系列回顾:第 1 篇 · BFF 架构 · Node.js 与 BFF 专题