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 稳定不变——解耦的价值就在这里。