如果你和我一樣,是2016年前後入行的前端,一定記得那個熱血沸騰的年代。
那時候,前端圈最響亮的口號是:「Node.js是前端的後端」。我們興奮地討論BFF、大前端,仿佛看到了前端工程師的未來——不再被後端牽著鼻子走,自己掌控整個資料鏈路。
我也是在那時候,第一次用Node.js搭起了BFF層。那種「前端也能寫後端」的掌控感,至今難忘。
然而最近兩年,風向變了。
很多中大廠都在悄悄「回退」Node.js中介層。有的把邏輯收歸後端,有的切到Serverless,有的乾脆砍掉整個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端不一樣。後端說:「你們前端能不能統一一下?」
你心裡一萬隻羊駝跑過:「明明是你接口設計不合理,怪我咯?」
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層加個快取。
// 後端接口字段名不合理?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的「降維打擊」。
我第一次接觸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剛出來時,我們覺得它不就是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解決了當時最痛的三個問題:
對於我們前端來說,BFF給了我們更大的話語權和自主權。它讓我們從「切圖仔」變成了「能掌控資料鏈路的人」。
這段經歷,也讓我們學會了後端思維:快取、並發、熔斷、限流...這些知識,現在依然在用。
如果你問我,現在要不要學Node.js中介層,我的答案是:要學,但不是以前那種玩法。
// 傳統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的設計思想
type User {
id: ID!
name: String!
orders: [Order!]!
}
type Order {
id: ID!
amount: Float!
status: String!
}
Schema優先、強類型契約、按需查詢——這些思想,會讓你對「前後端協作」有更深的理解。
即使以後不用BFF了,那段經歷也是寶貴的。你學會了如何處理並發、如何設計快取、如何做服務熔斷、如何排查線上問題。
// 這些能力依然有用
Promise.all([fetchA(), fetchB(), fetchC()]); // 并發控制
node --inspect-brk app.js // 調試技巧
這些能力,會讓你成為「更懂後端」的前端,在協作中更有話語權。
技術的世界,沒有永恆的真理,只有不斷變化的語境。
BFF從崛起到回落,不是一個失敗的故事,而是一個成長的印記。它見證了前端從「切圖」到「全棧」的探索,也見證了架構演進的必然規律。
對於我們每個親身經歷過的人來說,重要的是:不要停留在過去的榮光裡,也不要否定曾經的探索。
保持學習,保持思考,保持對新技術的好奇。
這才是我們前端最寶貴的品質。
如果你也經歷過BFF的起起落落,歡迎在評論區聊聊你的故事。