GraphQL:企业蜜月期结束
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 工作流:
- 调用下游服务
- 适配响应
- 返回 UI 所需的数据
GraphQL 工作流:
- 定义 schema
- 定义类型
- 定义 resolver
- 定义数据源
- 仍然要编写适配函数
- 保持 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 并不坏,只是适用范围有限。除非你的架构已经解决了它想要解决的问题,否则你可能根本不需要它。