BFF模式:为什么你的SPA需要一个"后端代理人"?

BFF模式:为什么你的SPA需要一个”后端代理人”?

BFF模式

引言:前端直接调用API的痛点

在传统的SPA架构中,前端直接调用后端API:

1
2
3
React App → 用户服务API
→ 订单服务API
→ 支付服务API

这种架构有几个问题:

  1. 安全问题:前端需要处理Token
  2. 性能问题:多次API调用
  3. 耦合问题:前端和后端紧密耦合

今天,我用白话讲讲BFF模式如何解决这些问题。

第一章:BFF是什么?

一句话解释

BFF(Backend For Frontend)就是:为前端专门定制的后端服务。

生活中的例子

没有BFF的世界

  • 你去银行办业务
  • 要分别去柜台、理财、贷款窗口
  • 每个窗口都要排队
  • 流程复杂,效率低

有BFF的世界

  • 你去银行办业务
  • 有一个专属客户经理
  • 客户经理帮你搞定所有事情
  • 你只需要和一个人沟通

BFF就像这个专属客户经理:前端只需要和BFF沟通,BFF负责和后端服务打交道。

第二章:BFF的工作原理

API网关架构

架构对比

传统架构

1
2
3
SPA → 用户服务
→ 订单服务
→ 支付服务

BFF架构

1
2
3
SPA → BFF → 用户服务
→ 订单服务
→ 支付服务

工作流程

获取用户订单列表

传统架构

1
2
3
4
1. SPA调用用户服务,获取用户信息
2. SPA调用订单服务,获取订单列表
3. SPA调用支付服务,获取支付状态
4. SPA组装数据,显示页面

BFF架构

1
2
3
4
5
6
1. SPA调用BFF,获取用户订单列表
2. BFF调用用户服务,获取用户信息
3. BFF调用订单服务,获取订单列表
4. BFF调用支付服务,获取支付状态
5. BFF组装数据,返回给SPA
6. SPA显示页面

代码示例

BFF服务(Node.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const express = require('express')
const axios = require('axios')
const app = express()

// 获取用户订单列表
app.get('/api/user-orders', async (req, res) => {
const userId = req.user.id

// 并行调用多个服务
const [user, orders, payments] = await Promise.all([
axios.get(`http://user-service/users/${userId}`),
axios.get(`http://order-service/orders?userId=${userId}`),
axios.get(`http://payment-service/payments?userId=${userId}`)
])

// 组装数据
const result = {
user: user.data,
orders: orders.data.map(order => ({
...order,
payment: payments.data.find(p => p.orderId === order.id)
}))
}

res.json(result)
})

app.listen(3000)

SPA调用

1
2
3
4
5
6
7
// 只需要调用BFF
const response = await fetch('/api/user-orders')
const data = await response.json()

// 直接使用,不需要自己组装
console.log(data.user)
console.log(data.orders)

第三章:BFF的优势

优势一:安全性

问题:前端直接调用API,需要处理Token。

BFF解决方案

  • Token存储在BFF(服务端)
  • 前端不需要接触Token
  • 安全性大大提高

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// BFF处理认证
app.use((req, res, next) => {
// 从Cookie中获取Token
const token = req.cookies.access_token

// 验证Token
const user = verifyToken(token)
if (!user) {
return res.status(401).json({ error: 'Unauthorized' })
}

// 将用户信息附加到请求
req.user = user
next()
})

优势二:性能

问题:前端多次调用API,网络开销大。

BFF解决方案

  • BFF聚合多个API调用
  • 前端只需要一次请求
  • 减少网络开销

性能对比

场景 传统架构 BFF架构
API调用次数 3次 1次
网络延迟 300ms 100ms
数据传输

优势三:解耦

问题:前端和后端紧密耦合,修改困难。

BFF解决方案

  • 前端只依赖BFF
  • 后端服务可以独立演进
  • 修改不影响前端

示例

1
2
3
4
5
用户服务从v1升级到v2

只需要修改BFF的调用逻辑

SPA代码不需要修改

优势四:灵活性

问题:不同前端需要不同的数据格式。

BFF解决方案

  • 每个前端有专属BFF
  • BFF根据前端需求定制数据
  • 灵活适配不同前端

示例

1
2
3
Web BFF → 返回完整数据
Mobile BFF → 返回精简数据
TV BFF → 返回特定格式

第四章:BFF的实现

技术选型

技术 优点 缺点
Node.js 轻量、异步 不适合CPU密集型
Go 高性能、并发 学习曲线陡
Java 成熟、稳定 相对重量级
Python 简单、易学 性能一般

实现步骤

步骤一:创建BFF服务

1
2
3
4
5
6
7
8
9
// package.json
{
"name": "bff-service",
"dependencies": {
"express": "^4.18.0",
"axios": "^1.4.0",
"cookie-parser": "^1.4.6"
}
}

步骤二:实现认证中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// middleware/auth.js
const jwt = require('jsonwebtoken')

const authMiddleware = (req, res, next) => {
const token = req.cookies.access_token

if (!token) {
return res.status(401).json({ error: 'No token provided' })
}

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.user = decoded
next()
} catch (error) {
return res.status(401).json({ error: 'Invalid token' })
}
}

module.exports = authMiddleware

步骤三:实现API聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// routes/user.js
const express = require('express')
const axios = require('axios')
const router = express.Router()

router.get('/profile', async (req, res) => {
const userId = req.user.id

try {
// 并行调用多个服务
const [user, orders, preferences] = await Promise.all([
axios.get(`http://user-service/users/${userId}`),
axios.get(`http://order-service/orders?userId=${userId}&limit=5`),
axios.get(`http://preference-service/preferences/${userId}`)
])

res.json({
user: user.data,
recentOrders: orders.data,
preferences: preferences.data
})
} catch (error) {
res.status(500).json({ error: 'Failed to fetch profile' })
}
})

module.exports = router

步骤四:配置CORS和Cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// app.js
const express = require('express')
const cors = require('cors')
const cookieParser = require('cookie-parser')

const app = express()

// CORS配置
app.use(cors({
origin: 'http://localhost:3000', // SPA地址
credentials: true // 允许Cookie
}))

// Cookie解析
app.use(cookieParser())

// 路由
app.use('/api', require('./routes/user'))
app.use('/api', require('./routes/order'))

app.listen(3001)

第五章:BFF的挑战

挑战一:额外的复杂性

问题:需要维护额外的BFF服务。

解决方案

  • 使用框架简化开发
  • 自动化部署
  • 监控和日志

挑战二:单点故障

问题:BFF成为单点故障。

解决方案

  • 多实例部署
  • 负载均衡
  • 健康检查

挑战三:性能开销

问题:额外的网络跳转。

解决方案

  • 缓存机制
  • 异步调用
  • 连接池

第六章:BFF vs API Gateway

概念对比

维度 BFF API Gateway
定位 为前端定制 统一入口
粒度 细粒度 粗粒度
数量 每个前端一个 通常一个
功能 API聚合、认证 路由、限流、监控

架构对比

BFF架构

1
2
Web SPA → Web BFF → 微服务
Mobile App → Mobile BFF → 微服务

API Gateway架构

1
2
Web SPA → API Gateway → 微服务
Mobile App → API Gateway → 微服务

混合架构

1
2
Web SPA → BFF → API Gateway → 微服务
Mobile App → BFF → API Gateway → 微服务

总结

核心要点

  1. BFF是为前端定制的后端服务
  2. 解决安全、性能、耦合问题
  3. 每个前端可以有专属BFF
  4. 需要额外的维护成本

适用场景

  • SPA应用:需要安全处理Token
  • 多端应用:Web、Mobile、TV等
  • 微服务架构:需要API聚合
  • 性能敏感:需要减少API调用

给开发者的建议

  • 评估是否需要BFF:不是所有项目都需要
  • 选择合适的技术栈:根据团队技能选择
  • 自动化部署:减少维护成本
  • 监控和日志:及时发现问题

BFF不是银弹,但在合适场景下,它是解决SPA认证和API聚合问题的利器。