明哥的朋友介绍的一单,一个创业团队想优化一下他们的架构设计,于是明哥拉上我一起去瞧了瞧。
客户的架构简单地可分为三层:前端是 React SPA、中间层是 Node.js BFF、后端是 Java。
最引人注目的是这个庞大的 BFF 中间层:
src/
├── routes/ # 87 个路由文件
├── controllers/ # 87 个 controller(一一对应)
├── services/ # 92 个 service
├── middleware/ # 15 个中间件
├── utils/ # 各种 helper
└── config/ # 3 套环境配置
这 87 个路由中,大部分是形如这样的:
// 伪代码
router.get(`/api/users/:id`, async (req, res) => {
try {
const result = await javaService.get(`/user-service/users/${req.params.id}`)
} catch (err) {
res.json({ code: -1, message: err.message })
}
})
就是透传模式,把前端的请求原封不动地转发给后端,再把后端的响应原封不动地返回给前端(参数校验后端做了)。
简单计算一下成本
服务器成本
BFF 中间层跑了 3 台 2C4G 云服务器,每台 280 元/月;监控告警服务 200 元/月。合计 1040 元/月。
人力成本
员工每周花在 BFF 上的时间约 10 ~ 15 小时:
- 后端新增接口后,BFF 需同步增加路由,约 4 小时;
- Bug 修复,约 3 小时;
- 升级依赖/安全补丁:约 3 小时;
- 写测试:约 3 小时
如果按时薪 100 来算,每月有六千多元是花费在这个“反向代理器”上。
隐形成本
后端改接口要及时通知维护中间层的工友,人的自觉性或记忆力有时候是不可靠的。
诞生的缘由
和技术负责人沟通了一下这个 BFF 中间层存在的原因:
- 后端接口返回格式不统一
- 跨域问题,后端微服务分别是不同的域名
- 一个页面要调数个接口拼数据,代替前端做接口聚合
如何调整
透传相关
使用 nginx 反向代理
location /api/user-service/ {
proxy_pass http://java-user-service:8080/;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/order-service/ {
proxy_pass http://java-order-service:8081/;
proxy_set_header X-Real-IP $remote_addr;
}
# ... 6 个微服务全部直连
另外,Nginx 加几行 header 解决跨域问题,认证则交给 Java 后端本来就有的鉴权中间件。
接口聚合
可以简单地在前端使用 Promise.all,甚至 React Query/SWR 已经有这个功能,另外还有缓存和错误重试等,比自己在 BFF 中间层手写更加健壮。
接口格式不统一
在前端拦截器(以 axios 为例)中进行格式统一。
api.interceptors.response.use(response => {
const data = response.data
return {
success: (data.code === 0 || data.status === 'ok'),
data: data.data || data.result,
message: data.message || data.error
}
})
但其实推动后端统一接口格式,是不是一个更好的解决方案?
不变的部分
除了透传模式外,还有一些涉及到条件分支、错误回退、事务补偿等的中间层逻辑,所以并不能将整个 BFF 中间层去掉。
总结
有些问题确实可以在 BFF 中间层一时解决,但随着项目发展,其维护成本远超收益。


![[译] 终于,JavaScript 有了安全的数组方法](https://img-1252058122.cos.ap-guangzhou.myqcloud.com/blog/article-cover/cmfkkq9a00001uhcg5k63cmt3.jpg)

