如果你和我一样,是2016年前后入行的前端,一定记得那个热血沸腾的年代。
那时候,前端圈最响亮的口号是:“Node.js是前端的后端”。我们兴奋地讨论BFF、大前端,仿佛看到了前端工程师的未来——不再被后端牵着鼻子走,自己掌控整个数据链路。
我也是在那时候,第一次用Node.js搭起了BFF层。那种“前端也能写后端”的掌控感,至今难忘。
然而最近两年,风向变了。
很多中大厂都在悄悄“回退”Node.js中间层。有的把逻辑收归后端,有的切到Serverless,有的干脆砍掉整个BFF。曾经自豪的技术栈,怎么就成了“成本中心”?
今天,想站在咱们前端的视角,聊聊这场“退潮”背后的真实故事。
一、当年我们为什么对BFF如此着迷?
因为,我们真的受够了
回想一下没有BFF的日子有多痛苦:
产品经理说:“详情页需要展示用户昵称、订单金额、商品列表。”
你打开接口文档,发现要调三个接口:/user/info、/order/detail、/product/list。三个接口调完,还要自己拼数据。
// 没有BFF时,前端要自己聚合数据
async function getOrderPage(orderId) {
// 串行调用三个接口
const user = await fetch(`/api/user/info?userId=123`);
const order = await fetch(`/api/order/detail?orderId=${orderId}`);
const products = await fetch(`/api/product/list?orderId=${orderId}`);
// 手动拼数据
return {
userName: user.name,
orderAmount: order.amount,
productList: products
};
}
更崩溃的是,App端要的字段和Web端不一样。后端说:“你们前端能不能统一一下?”
你心里一万只羊驼跑过:“明明是你接口设计不合理,怪我咯?”
BFF给了我们“全栈”的尊严
Node.js BFF的出现,像是给前端打开了一扇窗。
// BFF层:数据聚合、裁剪、适配
router.get('/web/order/detail', async (ctx) => {
// 并行调用,性能更好
const [user, order, products] = await Promise.all([
fetchUser(ctx.query.userId),
fetchOrder(ctx.query.orderId),
fetchProducts(ctx.query.orderId)
]);
// 为Web端定制返回格式
ctx.body = {
userInfo: { name: user.name, avatar: user.avatar },
orderInfo: { amount: order.amount, status: order.status },
productList: products.map(p => ({ id: p.id, name: p.name, price: p.price }))
};
});
// 为App端返回精简数据
router.get('/app/order/detail', async (ctx) => {
// 同样的数据来源,不同的返回结构
});
- 后端继续提供原子接口,保持他们所谓的“纯洁”
- 我们在Node层做聚合、裁剪、适配
- 前端只调Node层,拿到的就是“刚刚好”的数据
更重要的是,不用再求后端改接口了。
字段名不对?Node层改一下。缺少数据?Node层调个新接口。响应太慢?Node层加个缓存。
// 后端接口字段名不合理?BFF层一键改写
const user = await fetchUser(userId);
// 后端返回的是 user_name,前端要的是 userName
return { userName: user.user_name };
那种“自己说了算”的感觉,太爽了。
二、蜜月期过后,我们开始尝到苦果
但架构是有代价的,只是这个代价,当时我们没算清楚。
运维噩梦:第一个周末被叫起来修服务器的滋味
我记得特别清楚,那是2019年的一个周六早上。
手机突然狂震,群里炸了:线上订单页打不开了。我迷迷糊糊爬起来,登录服务器,发现Node进程挂了。重启,又挂。再看,内存泄露。
# 前端不熟悉的运维命令
top # 看CPU
free -m # 看内存
tail -f /var/log/nginx/error.log # 看nginx日志
journalctl -u node-app # 看系统日志
那天我在电脑前蹲了四个小时,查日志、看监控、dump内存快照...最后发现是一个第三方SDK有bug。
作为一个前端,我擅长的是CSS布局、组件通信、状态管理。服务器的负载均衡、内存监控、日志采集,这些我根本不熟。
但因为是“前端负责的BFF”,出了问题,只能自己扛。
重复劳动:每个项目都在写一样的代码
后来公司扩张,业务线越来越多。每条线都要BFF,于是我们建了一套又一套。
打开代码库,惊人的相似:
// 业务线A的BFF
router.get('/a/order/detail', async (ctx) => {
const data = await fetchData();
return { code: 0, data };
});
// 业务线B的BFF
router.get('/b/order/detail', async (ctx) => {
const data = await fetchData(); // 几乎一样的逻辑
return { code: 0, data };
});
// 业务线C的BFF
router.get('/c/order/detail', async (ctx) => {
const data = await fetchData(); // 又一遍
return { code: 0, data };
});
这种重复劳动,本质上是在浪费我们前端的价值。我们本该花时间研究组件复用、性能优化、用户体验,结果天天在写重复的数据聚合代码。
“数据对不上”的锅,永远是我们背
最憋屈的是扯皮的时候。
前端调BFF接口,返回的数据缺字段。产品问:谁的问题?
后端说:“我接口返回了,你自己去看。” BFF说:“我透传了,没动过。” 最后查出来,是后端某个服务升级,字段名改了。
// 后端悄悄改了字段名,BFF层还在用旧的
// 后端返回:{ nickname: '张三' }
// BFF层还在用:user.name
// 前端收到:undefined
但沟通成本已经花了,时间已经耽误了,项目已经延期了。
三、杀死BFF的,不是后端,是新技术
如果说内部问题是“慢性病”,那新技术的出现,就是对BFF的“降维打击”。
Serverless:终于不用半夜修服务器了
我第一次接触Serverless,是帮朋友搞一个小程序。
// 云函数版本的BFF
exports.main = async (event, context) => {
const { userId, orderId } = event.query;
// 一样的聚合逻辑
const [user, order] = await Promise.all([
fetchUser(userId),
fetchOrder(orderId)
]);
return { user, order };
};
不用买机器、不用配nginx、不用考虑扩缩容。写完代码,serverless deploy,完事。出问题了?看日志,改代码,再部署。全程不用碰服务器。
而且成本低得惊人。以前BFF服务器7x24小时运行,半夜没人访问也在烧钱。Serverless按调用次数计费,低流量时期几乎不花钱。
// 传统BFF:一直运行
app.listen(3000, () => {
console.log('server running'); // 半夜也在运行
});
// Serverless:按需启动
exports.handler = async (event) => {
// 有请求才执行,执行完就销毁
return { statusCode: 200, body: 'hello' };
};
GraphQL:让前后端“吵架”变少了
GraphQL刚出来时,我们觉得它不就是BFF的另一种形式吗?但用了一段时间才发现,最大的改变是:前后端终于有了一份清晰的“契约”。
# 前端声明要什么
query {
order(id: "123") {
amount
status
user {
name
avatar
}
products {
name
price
}
}
}
// GraphQL resolver:聚合逻辑还在,但契约更清晰了
const resolvers = {
Order: {
user: (order) => fetchUser(order.userId),
products: (order) => fetchProducts(order.id)
}
};
以前调BFF接口,返回什么全靠看代码、靠猜。用GraphQL,前端明确声明要哪些字段,返回的数据结构是强类型的,IDE里还有智能提示。
后端终于“开窍”了
这几年,后端也在变化。
// 以前:后端坚持原子接口
GET /user/123
GET /orders?userId=123
GET /products?orderId=456
// 现在:后端提供聚合接口
GET /web/profile?userId=123
// 返回:{ user: {...}, recentOrders: [...], favoriteProducts: [...] }
后端团队也开始重视文档、规范字段命名、保证数据契约的稳定性。前端对BFF的依赖,自然就降低了。
四、但我们真的做错了吗?
写到这里,可能会觉得BFF是一个“错误的选择”。
但我想说:在那个时间点,BFF就是最好的解。
BFF解决了当时最痛的三个问题:
- 不用调N个接口了:一次请求,拿到所有数据
- 不同端可以定制数据了:Web、App、小程序各取所需
- 不用求后端改接口了:我们自己能改
对于我们前端来说,BFF给了我们更大的话语权和自主权。它让我们从“切图仔”变成了“能掌控数据链路的人”。
这段经历,也让我们学会了后端思维:缓存、并发、熔断、限流...这些知识,现在依然在用。
五、今天,我们前端该怎么玩?
如果你问我,现在要不要学Node.js中间层,我的答案是:要学,但不是以前那种玩法。
优先拥抱Serverless
// 传统BFF
const app = require('express')();
app.get('/api/order', async (req, res) => {
// 业务逻辑
});
app.listen(3000);
// Serverless版本
exports.handler = async (event) => {
// 同样的业务逻辑
return { statusCode: 200, body: JSON.stringify(data) };
};
除非有特殊需求,否则优先用云函数。运维成本几乎为零,咱们前端可以真正专注于业务逻辑。
学GraphQL,但别只会写resolver
// 理解GraphQL的设计思想
type User {
id: ID!
name: String!
orders: [Order!]!
}
type Order {
id: ID!
amount: Float!
status: String!
}
Schema优先、强类型契约、按需查询——这些思想,会让你对“前后端协作”有更深的理解。
把BFF当“学习后端思维”的跳板
即使以后不用BFF了,那段经历也是宝贵的。你学会了如何处理并发、如何设计缓存、如何做服务熔断、如何排查线上问题。
// 这些能力依然有用
Promise.all([fetchA(), fetchB(), fetchC()]); // 并发控制
node --inspect-brk app.js // 调试技巧
这些能力,会让你成为“更懂后端”的前端,在协作中更有话语权。
六、写在最后
技术的世界,没有永恒的真理,只有不断变化的语境。
BFF从崛起到回落,不是一个失败的故事,而是一个成长的印记。它见证了前端从“切图”到“全栈”的探索,也见证了架构演进的必然规律。
对于我们每个亲身经历过的人来说,重要的是:不要停留在过去的荣光里,也不要否定曾经的探索。
保持学习,保持思考,保持对新技术的好奇。
这才是我们前端最宝贵的品质。
如果你也经历过BFF的起起落落,欢迎在评论区聊聊你的故事。