4. BFF 层职责与边界:BFF 与后端服务的关系、BFF 与前端的关系、数据聚合与裁剪、会话管理边界
在微服务架构中,BFF(Backend For Frontend)层是前端与后端服务之间的“中间人”。明确 BFF 的职责与边界,是保障系统可维护性、避免职责混乱的关键。本章从四个核心维度展开:BFF 与后端服务的关系、BFF 与前端的关系、数据聚合与裁剪、会话管理边界。
4.1 BFF 与后端服务的关系
BFF 是后端服务的“编排者”与“代理”,而非“替代者”。BFF 不包含核心业务逻辑,也不直接操作数据库。其核心职责包括:
- 服务路由与聚合:将前端的一个请求,拆解为对多个后端微服务的调用,并聚合结果。
- 协议转换:将后端的 gRPC/Thrift/AMQP 协议转换为前端友好的 HTTP/REST 或 GraphQL。
- 错误处理与降级:对后端服务的超时、异常进行统一处理,返回前端可理解的错误结构。
- 限流与鉴权前置:在 BFF 层完成 Token 校验、频率限制,避免无效请求穿透到后端。
| 维度 | BFF 层 | 后端服务 |
|---|---|---|
| 业务逻辑 | 仅编排,不实现核心业务 | 实现领域核心逻辑 |
| 数据持久化 | 不直接访问数据库 | 负责数据读写 |
| 协议 | HTTP/GraphQL(面向前端) | gRPC/消息队列(内部) |
| 状态 | 无状态或轻量会话 | 有状态(数据库/缓存) |
| 部署频率 | 随前端需求频繁变更 | 按领域版本发布 |
实践建议:BFF 层应避免引入 ORM 或数据库连接池。一旦 BFF 开始写 SQL,就说明边界已经模糊,应重构为独立的后端服务。
4.2 BFF 与前端的关系
BFF 是前端的“专属适配层”。每个前端客户端(Web、iOS、Android、小程序)通常拥有独立的 BFF 实例,以确保:
- 接口契约独立:不同前端对数据格式、字段粒度、响应时间的要求不同,BFF 按需定制。
- 减少前端复杂度:前端无需关心后端服务拆分细节,只需调用 BFF 提供的聚合接口。
- 版本解耦:后端服务升级时,BFF 负责适配,前端代码无需同步修改。
BFF 与前端之间的通信应遵循以下原则:
- 接口粒度以页面/组件为单位:一个页面请求对应一个 BFF 接口,避免前端发起多次网络请求。
- 响应结构扁平化:BFF 返回的数据应直接可渲染,避免前端做深层数据转换。
- 错误码统一:BFF 将后端各种错误码映射为前端可展示的提示文案或状态码。
边界红线:BFF 不应包含 UI 逻辑(如按钮显隐、排序规则),这些应保留在前端。BFF 只负责“提供数据”,不负责“决定如何展示”。
4.3 数据聚合与裁剪
数据聚合与裁剪是 BFF 最核心的“增值”能力。聚合解决“如何拼”,裁剪解决“给多少”。
4.3.1 数据聚合模式
- 并行聚合:同时调用多个无依赖的后端服务,使用
sync.WaitGroup或errgroup并发等待。 - 串行聚合:后一个请求依赖前一个请求的结果,如先查用户 ID,再查订单列表。
- 混合聚合:部分并行、部分串行,适用于复杂页面。
4.3.2 数据裁剪策略
- 字段白名单:BFF 只返回前端需要的字段,避免传输冗余数据。
- 分页与截断:对列表类数据强制分页,防止一次返回过多数据。
- 敏感字段过滤:在 BFF 层移除密码、手机号等敏感信息,确保前端无法获取。
// Go 示例:BFF 层数据聚合与裁剪
type UserOrderResponse struct {
UserID string `json:"user_id"`
UserName string `json:"user_name"`
Orders []Order `json:"orders"`
TotalPage int `json:"total_page"`
}
func (b *BFFService) GetUserOrders(ctx context.Context, userID string, page int) (*UserOrderResponse, error) {
// 1. 并行获取用户信息和订单总数
var userInfo *UserInfo
var total int64
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
var err error
userInfo, err = b.userSvc.GetUser(ctx, userID)
return err
})
g.Go(func() error {
var err error
total, err = b.orderSvc.CountOrders(ctx, userID)
return err
})
if err := g.Wait(); err != nil {
return nil, err
}
// 2. 串行获取分页订单(依赖 total 计算分页)
orders, err := b.orderSvc.ListOrders(ctx, userID, page, pageSize)
if err != nil {
return nil, err
}
// 3. 裁剪:只返回前端需要的字段
return &UserOrderResponse{
UserID: userInfo.ID,
UserName: userInfo.Name, // 裁剪掉 phone, email
Orders: orders,
TotalPage: int(math.Ceil(float64(total) / float64(pageSize))),
}, nil
}
常见陷阱:在 BFF 层做数据聚合时,容易忽略超时控制。每个后端调用都应设置独立的超时,且总超时应小于前端等待超时。建议使用
context.WithTimeout 为每个子调用设置超时。
4.4 会话管理边界
会话管理是 BFF 层最容易“越界”的领域。BFF 应管理“会话凭证”,而非“会话状态”。
4.4.1 BFF 的会话职责
- Token 校验与解析:验证 JWT/Session ID 的合法性,解析用户身份信息。
- Token 续期:在 Token 即将过期时,自动刷新并设置新的 Cookie/Header。
- 会话上下文传递:将解析后的用户 ID、角色等信息注入到 gRPC 元数据或 HTTP Header 中,传递给后端服务。
4.4.2 会话管理的边界红线
| 属于 BFF | 不属于 BFF |
|---|---|
| Token 校验与解析 | 用户登录/注册(应属于 Auth 服务) |
| Token 刷新 | 密码修改/重置 |
| 会话上下文透传 | 会话持久化存储(如 Redis 存 Session) |
| 登出(清除 Cookie) | 权限变更通知 |
最佳实践:BFF 层应使用无状态 JWT,避免在 BFF 内存或本地缓存中维护会话状态。这样 BFF 实例可以随意扩缩容,不会丢失会话。
4.5 边界总结:BFF 的“该做”与“不该做”
| 职责域 | 该做(BFF 职责) | 不该做(越界行为) |
|---|---|---|
| 与后端关系 | 服务编排、协议转换、错误聚合 | 直接操作数据库、实现业务规则 |
| 与前端关系 | 接口定制、数据裁剪、版本适配 | 包含 UI 逻辑、决定组件显隐 |
| 数据聚合 | 并行/串行调用、字段白名单 | 数据排序、复杂计算、数据持久化 |
| 会话管理 | Token 校验、解析、透传 | 密码管理、会话持久化、权限变更 |
核心原则:BFF 是“薄层”,越薄越稳定。每次在 BFF 中增加新逻辑前,先问自己:“这个逻辑是否应该放在后端服务中?” 如果答案是“是”,则坚决不放入 BFF。