什么是 BFF?用 Node.js 做中间层要注意什么?
一句话回答
BFF(Backend For Frontend)是为某一种前端专门服务的后端层:Web、App、小程序各有一个,由前端团队用 Node.js 维护。它把多个后端微服务的接口聚合成页面需要的一个接口,裁剪掉不需要和不该暴露的字段,做鉴权转换(比如把用户的 Cookie 会话换成调用内部服务的令牌),也可以承担 SSR。代价是多了一跳网络延迟和一个可能出故障的节点,还容易把业务逻辑写进 BFF。实现上要做到:下游调用并发进行,每个调用都有超时,用 Promise.allSettled 区分核心数据和可降级的数据,配合熔断和缓存,避免某个下游变慢拖垮整个 BFF。
详细解析
为什么需要 BFF
后端按业务领域拆成了用户、订单、商品、推荐等微服务,接口是通用的;而前端的页面是按场景组织的,一个"我的"页面要用到好几个服务的数据:
- 前端直接调用:一个页面发五六个请求,移动网络下很慢;还要在前端拼数据,各端重复实现一遍
- 让后端为每个页面写聚合接口:后端团队变成前端的"接口工厂",需求排队
- BFF:由前端团队维护一个贴近页面的接口层,按页面需要组合下游接口,迭代节奏由前端自己掌握
Web App 小程序
│ │ │
Web BFF App BFF 小程序 BFF ← 前端团队维护,按端裁剪
└────────┼─────────┘
API 网关 / 服务发现
┌──────┬──────┬──────┐
用户 订单 商品 推荐 ← 后端微服务,提供通用的领域接口
BFF 的职责和边界
| 适合放在 BFF | 不适合放在 BFF |
|---|---|
| 聚合多个接口、组装成页面需要的结构 | 核心业务规则(价格计算、库存扣减、权限判定的依据) |
| 裁剪字段:只返回页面要的,去掉敏感字段 | 直接读写其他服务的数据库 |
| 鉴权转换:校验用户会话,换成内部调用的凭证 | 长时间运行的任务和复杂计算 |
| 按端适配:App 要小图,Web 要大图 | |
| SSR、接口级缓存、灰度和 A/B 的分流 |
判断标准:这段逻辑换一个端还需不需要?需要的话,它属于后端服务,否则不同 BFF 会各写一份、逐渐不一致。
风险
- 多一跳延迟:每个请求都多经过一次 BFF。BFF 和后端服务要部署在同一个机房或内网,下游调用要复用连接(keep-alive)
- 单点和级联故障:所有流量都经过 BFF,它挂了整个端不可用;某个下游变慢,会让 BFF 的请求和连接堆积,进而拖垮其他接口。要多实例部署,并做好超时、熔断和限流
- 业务逻辑泄漏:为了赶需求,在 BFF 里写业务判断,最后变成另一个难以维护的"大后端"
- 团队能力:前端团队要承担服务端的运维职责,包括监控、告警、容量规划、值班
聚合的写法
- 并发:互不依赖的下游调用用
Promise.all/Promise.allSettled同时发出,总耗时约等于最慢的那个,而不是所有调用之和 - 区分核心和非核心:核心数据(用户信息)失败,整个接口失败;非核心数据(推荐、广告)失败,降级为空,页面照常展示。
Promise.all只要有一个失败就整体失败,这里要用allSettled逐个判断 - 每个调用都设超时:非核心数据的超时应该更短,宁可不展示,也不能拖慢整个页面
- 熔断:某个下游持续失败时,暂时不再调用它,直接走降级逻辑,给它恢复的时间,原理见 熔断、降级和限流
- 缓存:变化慢的数据(配置、商品类目)在 BFF 做短时间缓存,减少对下游的压力
GraphQL 是另一种思路:让前端在一次请求里声明需要哪些字段,由 GraphQL 服务负责聚合和裁剪,适合字段组合非常多变的场景,但要额外处理查询复杂度限制和 N+1 问题。
代码示例
// 带超时的下游调用:超时或非 2xx 都当作失败
async function callJson(url, timeoutMs) {
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) })
if (!res.ok) throw new Error(`${url} 返回 ${res.status}`)
return res.json()
}
// "我的"页面:聚合三个下游
export async function getProfilePage(userId) {
const [user, orders, recommend] = await Promise.allSettled([
callJson(`${USER_SVC}/users/${userId}`, 500),
callJson(`${ORDER_SVC}/orders?userId=${userId}`, 800),
callJson(`${RECOMMEND_SVC}/recommend?userId=${userId}`, 300), // 非核心,超时更短
])
if (user.status === 'rejected') throw user.reason // 核心数据失败,整个接口失败
return {
user: { id: user.value.id, name: user.value.name }, // 裁剪:敏感字段不出 BFF
orders: orders.status === 'fulfilled' ? orders.value : [],
recommend: recommend.status === 'fulfilled' ? recommend.value : [],
// 告诉前端哪些模块降级了,方便展示提示和统计
degraded: [orders, recommend].some((r) => r.status === 'rejected'),
}
}
推荐服务即使要 2 秒才返回,也会在 300ms 时被放弃、推荐模块为空;整个接口最多等待约 800ms(最长的那个超时),不会被推荐服务拖慢。
面试官可能追问
BFF 和 API 网关有什么区别?
API 网关是所有流量的统一入口,负责和业务无关的通用能力:路由转发、认证、限流、日志、TLS 终止,一般由基础架构团队维护,所有端共用。BFF 面向某一个端,负责和页面相关的聚合和裁剪,由前端团队维护。常见的部署方式是网关在前、BFF 在后,两者可以共存。
超时时间怎么定?
从页面能接受的总耗时倒推:比如接口整体要在 1 秒内返回,扣掉 BFF 自身的处理时间,再按每个下游的历史 P99 耗时分配,核心依赖给得宽一些,非核心的给得紧一些。超时之后不要立刻无限重试,重试会放大下游的压力;需要重试时限制次数,只对幂等的读请求重试,并让总耗时不超过上游给的预算。
什么时候不需要 BFF?
只有一个前端、后端是单体应用、接口本来就是按页面设计的;或者团队里没有人能承担 Node.js 服务的运维。这时 BFF 只会增加一跳延迟和一个要维护的服务。引入它的前提是:确实有多端差异或频繁的聚合需求,并且前端团队愿意对线上服务负责。
易错点
- 串行地
await互不依赖的下游调用,总耗时变成各个调用之和 - 用
Promise.all聚合,一个非核心接口失败导致整个页面报错 - 下游调用没有超时,一个慢服务让 BFF 的连接和内存不断堆积
- 在 BFF 中实现核心业务规则,导致多个端的逻辑不一致
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。