gRPC 网关与 BFF:前后端解耦的正确姿势

微服务内部用 gRPC(高效、强类型),但前端要的是 REST/JSON。中间这层怎么设计?BFF(Backend For Frontend) 和 网关 是两种答案。这篇文章是我们在 gRPC 微服务化后的对外接口层设计。

一、问题:内部 gRPC,外部要 REST

微 微 微 直 服 服 服 接 务 务 务 暴 ❌ ❌ ❌ 露 A B C 前 内 内 ( ( ( g 端 部 部 g g g R 生 接 数 R R R P 态 口 据 P P P C 对 太 结 C C C ? 细 构 ) ) ) g ( 暴 R 前 露 P 端 ( ─ ─ ─ C 要 耦 ┐ ┼ ┘ 合 ─ 支 1 、 ─ 持 改 差 个 内 前 ( 页 部 端 浏 面 就 要 还 览 , 改 什 要 器 g 前 么 : 不 R 端 ? 聚 原 P ) → 合 生 C 、 支 J 裁 持 要 S 剪 ) 调 O 、 N 鉴 8 权 A 、 个 P 限 ) I 流 ( R E S T )

二、两种模式

模式 A:网关(统一入口)

前 端 → R E S T A P I → 网 关 → g R P C ├ ├ └ ─ ─ ─ 微 服 路 协 聚 务 由 议 合 、 转 ( 鉴 换 可 权 ( 选 、 R , 限 E 简 流 S 单 T 聚 合 ↔ ) g R P C )

适用:对外统一入口、跨服务路由、通用横切(鉴权/限流/审计)。

模式 B:BFF(前端专属后端)

W A e p b p 前 前 端 端 → → B B F F F F - - W A e p b p → → g g R R P P ├ ├ └ C C ─ ─ ─ 微 微 聚 裁 适 服 服 合 剪 配 务 务 ( ( ( 按 只 前 前 给 端 端 前 特 页 端 殊 面 要 逻 组 的 辑 装 字 ) ) 段 )

适用:多端(Web/App/小程序)差异大、页面级聚合多、前端体验定制。

三、我的实践:网关 + BFF 分层

前 端 → R E S T → 网 关 ( 路 由 / 鉴 权 / 限 流 / 审 计 ) → B F F ( 聚 合 / 裁 剪 ) → g R P C 服 务

为什么两层都要:

网 B 单 单 关 F 网 管 F 关 B " : F 横 管 聚 F 切 " 合 : " 业 逻 鉴 ( 务 辑 权 所 " 堆 限 有 ( 在 流 接 页 网 每 口 面 关 端 统 级 各 一 聚 → 做 的 合 一 : 、 网 遍 鉴 字 关 权 段 变 → 、 裁 成 限 剪 " 重 流 、 上 复 、 前 帝 审 端 服 计 适 务 、 配 " 版 ) 本 )

四、BFF 的落地

gRPC 网关生成(协议转换)

1
2
3
4
5
6
7
8
9
// proto 定义 REST 映射(grpc-gateway 注解)
service ResumeService {
  rpc GetResume(GetResumeReq) returns (Resume) {
    option (google.api.http) = {
      get: "/v1/resumes/{id}"
    };
  }
}
// 生成:HTTP 处理器 → 转 gRPC 调用(自动完成 REST↔gRPC)

BFF 聚合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// BFF:一次前端调用,聚合多个 gRPC 服务
func GetProfilePage(ctx, req) (*ProfileVO, error) {
    // 并行调三个服务
    resume, _ := resumeClient.GetResume(ctx, req)
    apply, _ := applyClient.GetApplications(ctx, req.UserId)
    view, _ := statClient.GetProfileViews(ctx, req.UserId)
    // 组装前端要的结构(裁剪字段)
    return &ProfileVO{
        Resume: resume,
        ApplyCount: len(apply),
        Views: view.Count,
    }, nil
}

关键:

✅ ✅ ✅ ✅ 并 裁 B 错 行 剪 F 误 聚 字 F 包 合 段 装 ( ( 无 ( g 只 业 服 R 返 务 务 P 回 逻 错 C 前 辑 误 端 ( 并 要 只 → 发 的 是 调 , 聚 前 用 减 合 端 , 少 + 友 不 传 适 好 等 输 配 错 串 ) , 误 行 别 码 ) 在 ) B F F 写 业 务 )

五、踩坑记录

  1. BFF 变成业务层:聚合逻辑越写越多,最后 BFF 重写了业务 → BFF 只做"组装和裁剪",业务在服务层
  2. 聚合串行:3 个 gRPC 调用串行,页面延迟 = 3 倍 → 并行 + 超时控制(总超时 500ms)
  3. 网关鉴权重复:网关鉴权了,BFF 又鉴权 → 双层鉴权(网关验 token,BFF 验上下文,职责不同)
  4. gRPC 错误映射:gRPC status 直接透传给前端(看不懂)→ 映射为 HTTP 状态码 + 业务错误码

六、什么时候不需要 BFF

✅ ✅ ✅ 需 要 单 接 团 ❌ ❌ ❌ 端 口 队 B ( 简 小 F 前 多 前 只 单 : F 端 端 端 有 ( B 要 ( 频 一 C F 的 的 W 繁 个 R F 信 " e 催 U 号 页 b " W D 是 : 面 / 能 e 额 数 A 不 b 直 外 据 p 能 ) 接 维 " p 给 : 映 护 要 ) 个 网 射 成 调 返 聚 关 ) 本 回 合 : 3 结 接 + 不 + 构 口 需 不 " 直 要 个 同 接 聚 服 合 务 g R P C 网 关 映 射 就 够

总结

gRPC 网关与 BFF 的核心认知:

  1. 网关管横切(鉴权/限流/审计),BFF 管聚合(页面组装/裁剪)
  2. gRPC 网关自动转换协议:proto 注解生成 REST 映射
  3. BFF 是"薄层":只组装裁剪,别写业务
  4. 按需引入:单端简单接口不需要 BFF

对外接口层是微服务的"门面":网关+BFF 分层,让内部 gRPC 随便演进,前端 API 稳定不变——解耦的价值就在这里。