GraphQL:企业蜜月期结束

发布: (2025年12月15日 GMT+8 01:13)
9 min read

Source: Hacker News

引言

我在一个真正的企业级应用中使用了 GraphQL,尤其是 Apollo Client 和 Server,已经有好几年了。不是玩具应用,也不是绿地创业项目,而是一个拥有多个团队、BFF、下游服务、可观测性需求以及真实用户的正式生产环境。

经过这么长时间,我得出了一个相当无聊的结论:

GraphQL 解决了一个真实的问题,但这个问题比人们承认的要小得多。在大多数企业环境中,这个问题已经在其他地方得到了解决,而当你把所有权衡加在一起时,GraphQL 往往会成为净负面。

这不是一篇“GraphQL 不好”的文章,而是一篇“GraphQL 蜜月期结束”的文章。

GraphQL 试图解决的核心问题

GraphQL 试图解决的主要问题是 过度获取(over‑fetching)。这个想法既简单又诱人:

  • 客户端只请求它真正需要的字段
  • 不多也不少
  • 没有浪费的字节
  • 每次 UI 需求变化都不需要后端改动

纸面上听起来很棒,实际操作中却更为复杂。

过度获取已经被 BFF 解决

大多数企业前端架构已经拥有 BFF(Backend for Frontend)。BFF 的存在正是为了:

  • 为 UI 整理数据
  • 聚合多个下游调用
  • 隐藏后端复杂性
  • 返回 UI 所需的精确内容

如果你在 BFF 后面使用 REST,过度获取已经可以解决。BFF 可以对响应进行裁剪,只返回 UI 关心的部分。

是的,GraphQL 也能做到这一点。但大多数下游服务仍然是 REST,所以你的 GraphQL 层仍然需要从这些 API 中过度获取,然后再重新塑形响应。你并没有消除过度获取,只是把它搬到了下一层。这单独就大幅削弱了 GraphQL 的核心卖点。

有一种情况 GraphQL 能获胜:如果多个页面访问同一端点但需要的字段略有不同,GraphQL 允许你在每个查询中对这些差异进行裁剪。但这通常只为每次请求节省几字段,以换取:

  • 更多的搭建工作
  • 更多的抽象层
  • 更多的间接性
  • 更多需要维护的代码

为了几千字节的收益,这代价相当高。

实现时间远高于 REST

实现 GraphQL 的时间显著长于实现一个 REST BFF。

REST 工作流:

  1. 调用下游服务
  2. 适配响应
  3. 返回 UI 所需的数据

GraphQL 工作流:

  1. 定义 schema
  2. 定义类型
  3. 定义 resolver
  4. 定义数据源
  5. 仍然要编写适配函数
  6. 保持 schema、resolver 与客户端同步

GraphQL 通过牺牲生产速度来优化消费。在企业环境中,生产速度往往比理论上的优雅更重要。

可观测性默认更差

GraphQL 使用一种古怪的状态码约定:

  • 400 表示查询无法解析
  • 200 并带有 errors 数组表示执行过程中出现错误
  • 200 表示成功或部分成功
  • 500 表示服务器不可达

从可观测性的角度来看,这非常痛苦。使用 REST 时:

  • 2XX 表示成功
  • 4XX 表示客户端错误
  • 5XX 表示服务器错误

如果你在仪表盘中按 2XX 过滤,就知道这些请求成功了。而在 GraphQL 中,200 仍可能意味着部分或完全失败。Apollo 允许自定义此行为,但这会增加额外的配置、约定和认知负担——这是一种“调用时付费”的税,而不是博客文章里免费送的。

缓存听起来很棒,实际却很头疼

Apollo 的归一化缓存在理论上很吸引人。实践中,它却相当脆弱。如果两个查询仅在一个字段上不同,Apollo 会把它们视为两个独立查询,迫使你手动处理:

  • 从缓存中取出已有字段
  • 只为不同的字段发起请求

结果是:

  • 仍然会有一次往返
  • 增加代码复杂度
  • 调试缓存问题本身就成了一个新问题

与此同时,REST 可以轻松地多获取几个字段,缓存整个响应,然后继续工作。额外的几千字节成本低,复杂度却高。

ID 要求是一种泄漏的抽象

Apollo 默认要求每个对象都有 id_id 字段,或者你必须配置自定义标识符。许多企业 API:

  • 不返回 ID
  • 缺少自然唯一键
  • 并未建模为全局可识别的实体

因此 BFF 必须在本地生成 ID,仅仅是为了满足 GraphQL 客户端,这会带来:

  • 更多的逻辑
  • 更多的字段
  • 额外的获取字段(讽刺的是,这违背了最初减少过度获取的目标)

REST 客户端没有这种限制。

文件上传和下载很尴尬

GraphQL 并不适合二进制数据。实际使用中你会:

  • 返回一个下载 URL,然后使用 REST 去获取文件,或
  • 直接在 GraphQL 响应中嵌入大体积负载(例如 PDF),导致响应膨胀并影响性能

这破坏了“一站式 API”的说法。

入职培训更慢

大多数前端和全栈开发者对 REST 的经验远超 GraphQL。引入 GraphQL 意味着要教授:

  • schema
  • resolver
  • 查询组合
  • 缓存规则
  • 错误语义

这条学习曲线会产生摩擦,尤其在团队需要快速迭代时。REST 虽然乏味,却极其易于扩展。

错误处理比想象中更复杂

GraphQL 的错误响应……很奇怪。你会面对:

  • 可空 vs 非可空字段
  • 部分数据 + errors 数组
  • 带有自定义状态码的 extensions
  • 必须追踪是哪一个 resolver 失败以及原因

所有这些都增加了间接性。相比之下,简单的 REST 设置只需要:

  • 输入校验失败 → 400
  • 后端错误 → 500
  • 校验库(例如 Zod)报错 → 完事

简单的错误更容易推理,而不是优雅却复杂的错误。

最终结论

GraphQL 确实有其合理的使用场景。但在大多数企业环境中:

  • 你已经有了 BFF
  • 下游服务是 REST
  • 过度获取并不是最大的痛点
  • 可观测性、可靠性和速度更为重要

把所有因素加在一起,GraphQL 往往只解决了一个狭窄的问题,却引入了一系列更广泛的新问题。这也是为什么在生产环境使用多年后,我会说:

GraphQL 并不坏,只是适用范围有限。除非你的架构已经解决了它想要解决的问题,否则你可能根本不需要它。

Back to Blog

相关文章

阅读更多 »