我认为,GraphQL 将改变世界。将来,你可以使用 GraphQL 查询世界上的任何系统。我在创造这样的未来。那么我为什么要对使用 GraphQL 进行辩驳呢?我个人最讨厌的是,社区一直在宣传 GraphQL 的好处,而这些好处却非常普通,并且与 GraphQL 实际上没有任何关系。如果我们想推广采用,那么我们应该诚实,应该摘掉有色眼镜。这篇文章是对 Kyle Schrade 的文章“为什么使用GraphQL”的回应。这并不是批评。这篇文章是一个很好的讨论基础,因为它代表了我在社区中经常听到的观点。如果你读了整篇文章,当然这会花一些时间,你就会完全理解,为什么我认为 Kyle 的文章应该改名为“为什么使用 Apollo”。
如果你还没有读过 Kyles 的文章,我建议你先读一下。
本文最初发布于 WunderGraph 官方博客(《Why not use GraphQL?》),经原作者授权由 InfoQ 中文站翻译并分享。
REST 的缺点
作者指出了 REST API 的一系列缺点,以及 GraphQL 如何克服所有这些缺点:
过度获取;
多个请求请求多项资源;
针对嵌套数据的瀑布式网络请求;
每个客户端都需要知道每个服务的位置。
前三个问题可以通过另外编写一个 REST API 来解决。将新编写的 API 作为外观,用于特定的用户界面。以 Next.JS 为例。Next 提供了非常轻量级的语法定义 API。你可以将多个调用封装到一个 API 中,让它们在服务器端完成,而不是从客户端发出多个请求。此方法也可以解决过取和欠取问题,因为你可以在将数据发回客户端之前对其进行操作。这里所描述的模式称为“Backend For Frontend(BFF)”。它并不限于像 Next.JS 这样的全栈框架。你也可以为移动应用建立一个 BFF。
使用 BFF 模式,客户端不必知道每个服务的位置。但是,实现 BFF 的开发人员需要了解服务环境。希望你所有的服务都有接口定义规范,并在开发者门户中进行了展示。这样的话,编写 BFF 应该很容易。
GraphQL 需要开发人员实现解析器。实现解析器的任务与构建 BFF 差不多,逻辑非常相似。那么,真正的区别是什么呢?
BFF 更容易实现,因为有更多的工具可用。例如,如果你使用像 Next.JS 这样的框架,结合 swr hooks(stale while revalidate),你就可以直接使用 Etags 自动缓存和缓存失效。这减少了服务器和客户端之间发送的数据量,甚至比 GraphQL 更少,因为你没有发送查询负载,如果响应仍然有效,则服务器发回一个 304 响应(未修改)。此外,你无需使用像 Apollo 这样的重量级客户端。由 Vercel 提供的 swr 库很小,很容易使用。它支持分页、hooks,并能帮我们实现非常有效地来回导航。
GraphQL 已经持久化了查询,但是这种实现会带来额外的开销。如果你没有使用像 Relay 这样的客户端(它默认会持久化查询),则必须自己完成,或者使用一些第三方库来实现。与使用 Next.JS 的 BFF 方法相比,在前端获得相同的结果会更复杂。如何使用 GraphQL 实现 Etags?如果没有任何变化,如何使 GraphQL 服务器返回 304 状态码?你不是必须首先将所有查询转换为 GET 请求吗?要是那样的话,你的 GraphQL 客户端和服务器是否很容易支持这一点?
在用户体验和开发易用性方面,显然 BFF 是赢家。客户端和服务器之间传输的数据更少,实现更容易,客户端更小,活动部件更少。
但有一个问题。你必须为每个前端单独构建一个 BFF。如果你有很多前端,这可能是一项很大的工作。你必须运维所有的 BFF,对它们进行操作,保证它们的安全。
如果不需要权衡取舍就能获得两者的好处,那岂不是很好?这就是 WunderGraph 的用途所在,一个使用 GraphQL 构建 BFF 的框架。
减少 API 版本
在下一段中,Kyle 继续讨论了 API 版本相关的问题。他绝对是对的,API 的版本太多会使跟踪变得非常困难。然后他总结道,在 GraphQL 中,graph 只有一个版本,变更可以在模式注册表中跟踪,这是 Apollo 的一个付费功能。因为这个原因,你在版本控制方面不会有任何问题。
我无法认同这个结论,不能仅仅因为 GraphQL 模式不支持本地版本控制就说问题消失了。如果不为 REST API 设置版本,也会取得同样的效果。事实上,许多专家认为,如果不是非常必要,就应该尽量不引入 API 版本。话虽如此,是什么阻碍了你运行两个版本的 GraphQL 模式?我不认为这是个好主意,但技术上是可行的。
如果在你的组织中存在 REST API 版本过多的问题,那么在使用像 GraphQL 这样的新工具解决这个问题之前,也许你应该首先了解一下自己所在的组织,为什么会有这么多版本。也许修改下流程或团队结构会有所帮助?GraphQL 完全不能解决版本控制问题。相反,我认为它实际上使情况变得更糟。
你必须支持移动应用程序吗?你应该意识到,发布原生应用需要时间。你必须等待应用商店的审批,你会发现,许多用户永远不会(或缓慢地)安装新版本。在这种情况下,如果想引入一个破坏性更改,而又不破坏客户端,该怎么办?这是不可能的。你必须以一种非破坏性的方式引入此更改。了解下 Facebook 是如何避免破坏客户端的,这很有趣。
在 GraphQL 的情况下,模式演进意味着弃用一个旧字段,并添加一个新字段。新客户端使用新字段,而你希望使用旧字段的客户端越来越少。但愿,你已经有了一个可以强制用户在某个时间下载新版本的系统。否则,你可能会被迫无限期地支持弃用的字段。如果是这种情况,GraphQL 的弃用模型对你一点帮助都没有。
使用 REST,你可以创建一个新端点或现有端点的另一个版本。问题是一样的,只是解决方案看起来有一点不同。
需要说明的是,如果你无法控制客户端,确实就需要某种版本控制。如果你只有一个 Web 应用程序,就不需要此功能。但那时,GraphQL 可能也就不必要了。
降低有效载荷
在这一段中,作者指出,RESTful API 不允许部分响应。这是错误的。这里有一个例子:
作者到底想表达什么意思?我很确定他了解部分响应。我猜他想说的是,部分响应需要有人实现。实际上,这与你在 GraphQL 中从一个资源里选择子字段非常类似。在 GraphQL 中,这是个开箱即用的特性。
另一方面,对于 BFF 方法,你不需要这样。只需要返回你需要的数据即可。同样,使用像 Next.JS 这样的全栈框架更容易实现这一点,缓存也更容易,同时你还免费获得了基于 Etag 的缓存失效。
综上所述,GraphQL 准确提供了你所需的数据。部分响应也能达到同样的效果。BFF 需要付出额外的实现和维护成本,但 UX 和 DX 更好。
强类型接口
在这一段中,Kyle 指出了 REST API 类型不严格的问题。他谈到了 API 的问题,即你不清楚获得的是一组帖子,还是其他的什么东西,以及查询参数如何使情况变得更加复杂。他还指出,由于 GraphQL 具有严格的类型系统,所以就不存在这个问题。
我认为 Kyle 所谈论的是一个组织问题,需要一个组织层面上的解决方案。
当你允许开发人员部署 REST API 而又没有发布接口定义规范(Open API specification,OAS)或类似的内容时,就会遇到他所描述的那种问题。使用 OAS,可以非常容易地描述所有资源。在 OAS 中还可以描述 OAuth2 流和每个端点的作用域。此外,你还可以描述查询参数的确切类型和验证规则,这是 GraphQL 所没有的特性。
GraphQL 无法描述身份验证、授权和输入验证。GraphQL 没有这些特性,因为来自 Facebook 的发明者从另一个层面解决了这个问题。他们不需要向 GraphQL 添加这些特性。你可以向模式添加自定义指令,以获得类似于 OAS 的结果,但你必须得自己维护这样的自定义实现。
你可能会认为,OAS 不能保证 API 的响应符合规范。你说的对。但是,GraphQL 模式是如何保证的呢?
GraphQL 内省是向服务器发送一个特定的 GraphQL 查询以获取关于 GraphQL 模式的信息。GraphQL 服务器可以自由地使用它期望的任何类型进行响应。如果你发送一个查询,则服务器的响应可以不符合自省响应中的 GraphQL 模式。以 Apollo Federation 为例。你将模式上传到模式注册中心,然后因为错误部署了 GraphQL 服务器的错误版本。如果你更改字段的类型,客户端可能就无所适从了。
当我们讨论 GraphQL 中的类型安全时,其实我们的意思是,我们相信 GraphQL 服务器的行为会与自省查询响应保持一致。为什么我们不能同样信任接口定义规范呢?我想我们可以。如果我们不那样做,那就是人的问题,而不是技术的问题。
提高客户端性能
接下来的一段很短,是关于 GraphQL 如何提高客户端性能和减少网络往返。
对于 BFF 多么强大,以及与重量级的 GraphQL 客户端相比,我们从“stale while revalidate 模式”中获得了多少好处,我想我已经解释得够多了。
也就是说,GraphQL 确实减少了请求数量以及总体的数据传输量。但是,你应该考虑向前端添加 GraphQL 客户端的成本。
减少记录和浏览 API 的时间
下一节将介绍如何将 OAS 这样的工具用于 RESTful API 开发,以及在微服务环境中维护多个 OAS 所面临的挑战。Kyle 将一个 GraphQL 模式和分散在多个 Git 存储库中的 Swagger 文件进行了对比。
我认为,浏览一个 GraphQL 模式显然比查看 Git 存储库中的多个 OAS 文件要简单许多。不过,公平起见,我们必须进行同等比较。如果希望提高开发人员的工作效率,就不要将 OAS 文件放入 Git 存储库,然后就收工了。你应该运营一个开发者门户,可以在上面搜索并浏览 API。
OAS 依赖于 JSON-Schema,它有一个了不起的特性:可以从另一个文档引用对象类型。你可以将 OAS 分割为多个文件,如果需要,这些文件之间可以相互引用。还有工具可以将多个 OAS 文件合并成一个 OAS 文档。然后,你可以使用这个文档并将其提供给开发者门户,你可以在上面浏览所有的 API。请记住,所有这些都需要额外的成本。你需要运营或购买一个开发门户。你必须描述所有的 API,至少在开始时,这会是一种负担。
需要补充的一点是,有很多框架允许你用自己喜欢的编程语言描述模式,比如,通过定义对象或类。然后,你可以获得一个自动生成的接口定义规范,通过一个大家都知道的端点提供出来。
让我们和 GraphQL 做个比较。GraphQL 提供了两种基本方法:SDL 优先和代码优先。无论使用哪种,你最终都会得到一个 GraphQL 模式,它描述了所有的类型和字段,并允许你对它们进行注释。
那么,有什么区别呢?OAS 设置开销更大。另一方面,OAS 内置支持记录示例用例、身份验证和授权以及输入验证规则。
请记住,GraphiQL 本身没有多 GraphQL 模式的概念。如果你有多个 GraphQL(微)服务,就必须运行或购买一个专用组件,例如一个模式注册表,类似于 REST API 的开发者门户。
我想专门用一个段落来介绍 API 用例。有了 OAS 或 GraphQL 模式并不意味着 API 就有了良好的文档记录。API 用户可以用 API 做什么?如何用?什么样的用例好?什么样的不好?在哪里寻求帮助?如何对用户进行身份验证?需要 API 密钥吗?要使 API 文档能帮助 API 消费者使用,所需做的工作比向类型和字段添加描述要多得多。OAS 允许你添加有效负载示例并描述它们。GraphQL 没有这个特性。Stripe 就是一个很好的例子。其表现远远超过了 Swagger 或 GraphQL Playground。
另一方面,如果你查看 GitHub 的公共 GraphQL API,就会发现没有一个查询示例。如果你想获得关于存储库及其所有问题的信息,就必须打开 GraphiQL 并开始搜索。然而,GraphiQL 的搜索功能并不能为你提供多少帮助。需要有人坐下来,编写如何使用 API 的示例查询和用例。否则,真的很难上手。
因此,尽管社区一直在说,“GraphQL 是自记录的”,但这个特性本身并不能成为一个有用的 API。OAS 提供了一个添加用例的工具,但是你仍然必须自己编写它们。无论你选择什么工具或语言,都需要努力使 API 对他人有用。
支持遗留应用
在这一段中,作者说,为移动应用保留 REST API 的旧版本是一件痛苦的事情。他总结说,因为我们只使用了一个 GraphQL 服务器,所以没有这个问题。
我很抱歉,但是我又一次得出了一个完全不同的结论。如果将规则设置为不允许版本控制,则可以添加新的端点或替换现有端点的实现。在这种情况下,GraphQL 和 REST 之间没有区别。对于 REST 和 GraphQL API 来说,支持遗留应用都是一项挑战。你需要找到一种方法,不能破坏客户端和服务器端之间的契约。不管服务器提供的是 REST 还是 GraphQL,问题都是一样的。
错误处理更好
关于错误处理,作者描述了这样一个场景:与使用部分数据响应的单个 GraphQL 查询相比,客户端将不得不进行 3 次连续的 REST API 调用。
使用 GraphQL,解析部分数据的逻辑位于服务器中。客户端需要有额外的逻辑对部分响应做相应的处理。
使用 REST,获取部分数据的逻辑位于客户端或 BFF 中。无论哪种方式,其逻辑都或多或少与 GraphQL 相同,只是位置不同。显然,REST API 用例也需要客户端中的逻辑来处理部分响应。这个逻辑与 GraphQL 用例中的逻辑几乎完全相同。
在 REST 响应中,你也可以返回有关失败原因的特定信息。OAS 允许使用复合类型,因此,你可以就部分响应向客户端提供丰富的信息。这类似于 Sascha Solomon 描述的响应复合的概念(https://sachee.medium.com/200ok-err-handling-in-graphql-7ec869aec9bc)。
GraphQL 的错误处理真的更好吗?我认为,OAS 和 GraphQL 都提供了不错的工具,让你可以用非常友好的方式处理错误。充分利用这些工具是开发人员的事。天下没有免费的午餐。
结论
Kyle 在总结全文时说,GraphQL 是 API 的未来,因为它在性能、有效负载大小、开发时间和内置文档方面都很出色。
我赞同,GraphQL 是 API 的未来,但是出于不同的原因。
较好的性能和较小的有效负载并不是 GraphQL 特有的特性。你可以使用其他工具,或者扩展 GraphQL,来获得更好的结果,例如使用 Relay 来持久化查询。要真正地从 GraphQL 文档中获得好处,你要做的不仅仅是向模式中添加描述。
当尘埃落定,炒作消失,我们必须看清事实。明明 GraphQL 不是这样的,我们却要设法让人们相信它是这样的,我们不应该这么做。使用 GraphQL 有很多优点,但是那取决于你的用例,你可能不会真正从中受益。我们应该给出一个更细致的答复,而不是把 GraphQL 说成是圣杯。
解决问题的最佳方法是先看问题,然后对可能用来解决问题的工具进行差异比较。如果你的组织在实现 REST API 方面失败了,那么 GraphQL 如何解决这个问题?也许你的组织中有些东西需要改变?另一方面,如果这不是一个组织问题,并且你非常确信,对于你的用例,REST 不是最好的选择,那么我打赌你会喜欢 GraphQL 的开发体验。
我支持使用 GraphQL 的原因
GraphQL 本身几乎没什么用。是工具使得 GraphQL 如此强大。是社区,是我们!是 Apollo、Hasura、The Guild、FaunaDB、Dgraph、GraphCMS,我希望还有 WunderGraph,是它们使 GraphQL 变得如此强大。是 GraphiQL、各种 GraphQL 客户端、Schema Stitching、Federation。整个工具生态系统才是 GraphQL 成为新一代重要技术的原因。
更多的工具和服务可以增强生态系统。更强大的生态系统将带来更多的采用,这反过来又吸引更多的公司向 GraphQL 生态系统添加服务和工具,这是一个非常积极的循环。
在这方面,GraphQL 非常类似于 Kubernetes。仅有容器运行时 Docker 是不够的。将复杂的系统调用封装到一个简单的 API 中是一个促成因素,但是为了创建一个丰富的生态系统,就需要有一个具有足够表达能力并可以轻松扩展的调度器。
GraphQL 为我们提供了一种与 Docker 一样简单的 API 定义和消费语言。如果你以前接触过 Javascript 和 JSON,那么马上就可以熟悉 GraphQL。但与 Docker 类似,该语言本身并没有那么强大。它的强大源于其可扩展性和周边工具。
由 Facebook 开源并不是该语言成功的原因。像 Relay 这样的工具才是。不幸的是,其中许多工具是内部使用的,从未公开。社区必须要设法跟上,在这方面,我认为我们已经做了很多。
我的结论
当 Kyle 问“为什么要用 GraphQL”时,我想他实际上是在说“为什么要用 Apollo”。答案很简单。没有人愿意围绕 REST API 构建一个丰富的生态系统。设法从接口定义规范生成一个 React.JS 客户端。与 GraphQL 客户端提供的体验相比,这种体验糟透了。没有人想出一个好的商业模式来解决这个问题。进入 GraphQL 生态系统,你将获得大量的工具,它们抽象出了那些你不想处理的问题。
对于所描述的用例,REST API 将变得边缘化,这并不是因为 GraphQL 更好。是工具和生态系统将使 GraphQL 的市场份额继续增加。与 REST 相比,GraphQL 生态系统的扩张速度非常快。
在服务器渲染的 Web 应用程序中,Hypermedia API 发挥了并将继续发挥重要的作用。然而,网络在向前发展。用户希望从网站获得一种原生体验。Jamstack 正在接管前端。服务器端渲染加动态 JavaScript 客户端的混合模型是这些应用程序的推动者。RESTful API 擅长解决另一类问题。它不会消失,恰恰相反!只是对于业界目前正在转向的这类应用程序,它们不是合适的工具。我认为,REST API 是内部 API、合作伙伴 API 和服务器间通信的完美工具。在这方面,与 REST 相比,GraphQL 并没有真正带来任何好处。与 RPC 一起,它在这个领域将会有一个很好的未来。另一方面,GraphQL 非常适合封装基于资源和 RPC 的 API。
如果没有 Apollo 提供的所有工具,你认为 GraphQL 会变成现在这样吗?会议呢?GraphQL 峰会?Hasura 在线会议?Dgraph Lab 举办的 GraphQL in Space?The Guild 所做的大量开源贡献?GraphQL Galaxy?
我希望,对于为什么应该使用 GraphQL,本文可以让你有个更细致地了解。一个不那么天花乱坠的观点应该能让你更好地说服你的经理。
评论