GraphQL 入门:为什么我们需要一种新的 API(第 1 部分)
已发表: 2022-03-10在本系列中,我想向您介绍 GraphQL。 最后,您不仅应该了解它是什么,还应该了解它的起源、缺点以及如何使用它的基础知识。 在第一篇文章中,我不想跳入实现,而是想通过查看从过去 60 年的 API 开发(从 RPC 到现在)中吸取的经验教训,回顾一下我们是如何以及为什么会使用 GraphQL(和类似工具)的。 毕竟,正如马克吐温所描述的那样,没有新的想法。
“没有新想法这回事。这是不可能的。我们只是把很多旧想法放在一个心理万花筒里。”
——马克吐温在“马克吐温自己的自传:北美评论的章节”中
但首先,我必须向房间里的大象讲话。 新事物总是令人兴奋,但它们也会让人感到筋疲力尽。 您可能听说过 GraphQL,只是想:“为什么……”或者,您可能想的更像是,“我为什么要关心新的 API 设计趋势? 休息……很好。” 这些都是合理的问题,所以让我帮助解释为什么你应该关注这个问题。
介绍
必须权衡为您的团队带来新工具的好处与其成本。 有很多东西要衡量。 学习需要时间,转换从功能开发中带走的时间,以及维护两个系统的开销。 由于成本如此之高,任何新技术都必须更好、更快或更高效。 增量改进虽然令人兴奋,但不值得投资。 我想谈论的 API 类型,特别是 GraphQL,在我看来是向前迈出的一大步,并且提供了足够多的好处来证明成本是合理的。
与其先探索功能,不如将它们置于上下文中并了解它们是如何存在的。 为此,我将首先回顾一下 API 的历史。
RPC
RPC 可以说是第一个主要的 API 模式,它的起源可以追溯到 60 年代中期的早期计算。 当时,计算机仍然如此庞大和昂贵,以至于我们认为 API 驱动的应用程序开发的概念大多只是理论上的。 带宽/延迟、计算能力、共享计算时间和物理邻近性等限制迫使工程师考虑分布式系统而不是暴露数据的服务。 从 60 年代的 ARPANET,一直到 90 年代中期的 CORBA 和 Java 的 RMI,大多数计算机都使用远程过程调用 (RPC) 相互交互,这是一种客户端-服务器交互模型,客户端导致一个过程(或方法)在远程服务器上执行。
RPC 有很多优点。 它的主要原则是允许开发人员在远程环境中处理代码,就好像它在本地环境中一样,尽管速度要慢得多且可靠性较低,这会在其他不同和不同的系统中产生连续性。 像 ARPANET 的许多东西一样,它领先于时代,因为这种类型的连续性是我们在处理数据库访问和外部服务调用等不可靠和异步操作时仍在努力的目标。
几十年来,对于如何允许开发人员将这样的异步行为嵌入到程序的典型流程中,已经进行了大量的研究。 如果当时有诸如 Promises、Futures 和 ScheduledTasks 之类的东西可用,我们的 API 环境可能看起来会有所不同。
RPC 的另一个优点是,由于它不受数据结构的限制,因此可以为客户端编写高度专业化的方法,这些方法可以准确地请求和检索所需的信息,从而最大限度地减少网络开销和更小的负载。
然而,有些事情使 RPC 变得困难。 首先,连续性需要上下文。 RPC,按照设计,在本地和远程系统之间创建了相当多的耦合——你失去了本地和远程代码之间的界限。 对于某些领域,这是可以的,甚至像在客户端 SDK 中一样首选,但对于客户端代码不太了解的 API,它可能比更面向数据的东西灵活得多。
不过,更重要的是API 方法的扩散潜力。 理论上,RPC 服务公开了一个可以处理任何任务的小型 API。 在实践中,大量外部端点可以在没有太多结构的情况下增加。 随着团队成员的来来去去和项目的转变,随着时间的推移,需要大量的纪律来防止 API 重叠和重复。
确实,通过适当的工具和文档更改,就像我提到的那样,可以管理,但是在我编写软件的时候,我遇到过很少的自动文档和规范的服务,所以对我来说,这有点像红鲱鱼。
肥皂
出现的下一个主要 API 类型是 SOAP,它诞生于 90 年代末期的 Microsoft Research。 SOAP (简单对象访问协议)是一个雄心勃勃的协议规范,用于应用程序之间基于 XML 的通信。 SOAP 的目标是通过为复杂的 Web 服务创建结构良好的基础来解决 RPC,特别是 XML-RPC 的一些实际缺陷。 实际上,这只是意味着向 XML 添加行为类型系统。 可悲的是,它造成的障碍比它解决的要多,这一点可以从今天很少有新的 SOAP 端点被编写的事实证明。
“SOAP 是大多数人认为适度的成功。”
— 唐箱
SOAP 确实有一些好处,尽管它的冗长和可怕的名字令人难以忍受。 客户端和服务器之间的 WSDL 和 WADL(发音为“wizdle”和“waddle”)中的可执行合同保证了可预测的、类型安全的结果,并且 WSDL 可用于生成文档或创建与 IDE 和其他工具的集成。
SOAP 关于 API 演进的重大启示是它逐渐且可能无意中引入了更多面向资源的调用。 SOAP 端点允许您请求具有预定结构的数据,而不是考虑生成数据所需的方法(假设它是以这种方式编写的)。
SOAP 最显着的缺点是过于冗长。 没有大量工具几乎是不可能使用的。 您需要工具来编写测试、工具来检查来自服务器的响应以及工具来解析所有数据。 许多较旧的系统仍然使用 SOAP,但对工具的要求使其对于大多数新项目来说过于繁琐,而且 XML 结构所需的字节数使其成为服务移动设备或繁琐的分布式系统的糟糕选择。
有关更多信息,值得阅读 SOAP 规范以及来自原始团队成员之一 Don Box 的 SOAP 令人惊讶的有趣历史。
休息
最后,我们来到了当前的 API 设计模式:REST。 REST 由 Roy Fielding 在 2000 年的一篇博士论文中介绍,使钟摆朝着完全不同的方向摆动。 REST 在很多方面都是 SOAP 的对立面,并排看它们会让你觉得他的论文有点生气了。
SOAP 使用 HTTP 作为哑传输,并在请求和响应正文中构建其结构。 另一方面,REST 抛弃了客户端-服务器合同、工具、XML 和定制标头,用 HTTPs 语义替换它们,因为它是结构选择而不是使用 HTTP 动词与引用某个层次结构中的资源的数据和 URI 交互数据。
肥皂 | 休息 | |
---|---|---|
HTTP 动词 | 获取、放置、发布、修补、删除 | |
数据格式 | XML | 无论你想要什么 |
客户端/服务器合约 | 每天一整天! | 谁需要那些 |
类型系统 | JavaScript 有无符号短对吗? | |
网址 | 描述操作 | 命名资源 |
REST 完全明确地将 API 设计从建模交互更改为简单地建模域数据。 在使用 REST API 时完全面向资源,您不再需要知道或关心检索给定数据需要什么; 您也不需要了解有关后端服务实施的任何信息。

简单性不仅对开发人员有利,而且由于 URL 代表稳定的信息,因此很容易缓存,它的无状态性使其易于水平扩展,并且由于它对数据进行建模而不是预测消费者的需求,因此可以显着减少 API 的表面积.
REST 很棒,它的普遍性取得了惊人的成功,但与之前的所有解决方案一样,REST 也并非没有缺陷。 为了具体谈谈它的一些缺点,让我们看一个基本的例子。 假设我们必须构建一个博客的登录页面,该页面显示博客文章列表及其作者姓名。

让我们编写可以从普通 REST API 检索主页数据的代码。 我们将从包装资源的几个函数开始。
const getPosts = () => fetch(`${API_ROOT}/posts`); const getPost = postId => fetch(`${API_ROOT}/posts/${postId}`); const getAuthor = authorId => fetch(`${API_ROOT}/authors/${authorId}`);
现在,让我们来编排吧!
const getPostWithAuthor = postId => { return getPost(postId) .then(post => getAuthor(post.author)) .then(author => { return Object.assign({}, post, { author }) }) }; const getHomePageData = () => { return getPosts() .then(postIds => { const postDetails = postIds.map(getPostWithAuthor); return Promise.all(postDetails); }) };
所以我们的代码将执行以下操作:
- 获取所有帖子;
- 获取每个帖子的详细信息;
- 获取每个帖子的作者资源。
好消息是,这很容易推理,组织良好,并且每个资源的概念边界都绘制得很好。 令人遗憾的是,我们只发出了 8 个网络请求,其中许多是串行发生的。
GET /posts GET /posts/234 GET /posts/456 GET /posts/17 GET /posts/156 GET /author/9 GET /author/4 GET /author/7 GET /author/2
是的,您可以通过建议 API 可以有一个分页的/posts
端点来批评这个示例,但这会让人毛骨悚然。 事实仍然是,您经常拥有一组 API 调用,这些调用相互依赖以呈现完整的应用程序或页面。
开发 REST 客户端和服务器肯定比之前的更好,或者至少是更白痴的证明,但是自从菲尔丁的论文以来的二十年里发生了很多变化。 当时,所有的电脑都是米色的塑料; 现在它们是铝的! 不过说真的,2000 年接近个人计算爆炸的顶峰。 处理器的速度每年都会翻一番,网络也以令人难以置信的速度变得更快。 互联网的市场渗透率在 45% 左右,无处可去,只能上升。
然后,在 2008 年左右,移动计算成为主流。 借助移动设备,我们在一夜之间在速度/性能方面有效地倒退了十年。 2017 年,我们拥有近 80% 的国内和超过 50% 的全球智能手机普及率,现在是重新思考我们对 API 设计的一些假设的时候了。
REST 的弱点
以下是从客户端应用程序开发人员的角度批判性地看待 REST,尤其是在移动设备中工作的开发人员。 GraphQL 和 GraphQL 风格的 API 并不新鲜,也不能解决 REST 开发人员无法解决的问题。 GraphQL 最重要的贡献是它能够系统地解决这些问题,并且具有其他地方不容易获得的集成水平。 换句话说,它是一个“包含电池”的解决方案。
REST 的主要作者,包括 Fielding,在 2017 年底发表了一篇论文(对 REST 架构风格的反思和“现代 Web 架构的原则设计”),反思了 REST 的两个十年及其启发的许多模式。 对于任何对 API 设计感兴趣的人来说,这本书很简短,绝对值得一读。
借助一些历史背景和参考应用程序,让我们看看 REST 的三个主要弱点。
REST 很健谈
REST 服务往往至少有点“健谈”,因为它需要在客户端和服务器之间进行多次往返才能获得足够的数据来呈现应用程序。 这种级联的请求会对性能产生破坏性影响,尤其是在移动设备上。 回到博客示例,即使在使用新手机和具有 4G 连接的可靠网络的最佳情况下,在下载第一个数据字节之前,您仅在延迟开销上花费了近 0.5 秒。
55ms 4G 延迟 * 8 个请求 = 440ms 开销

聊天服务的另一个问题是,在许多情况下,下载一个大请求所需的时间比下载许多小请求所需的时间少。 小型请求的性能下降确实有很多原因,包括 TCP 慢启动、缺乏标头压缩和 gzip 效率,如果您对此感到好奇,我强烈建议您阅读 Ilya Grigorik 的高性能浏览器网络。 MaxCDN 博客也有很好的概述。
这个问题在技术上不是 REST 而是 HTTP,特别是 HTTP/1。 无论 API 风格如何,HTTP/2 几乎都解决了聊天问题,并且它在浏览器和原生 SDK 等客户端中得到了广泛的支持。 不幸的是,API 方面的推出速度很慢。 在排名前 10k 的网站中,截至 2017 年底,采用率约为 20%(并且还在攀升)。令我惊讶的是,即使是 Node.js,也在其 8.x 版本中获得了 HTTP/2 支持。 如果你有能力,请更新你的基础设施! 同时,让我们不要停留,因为这只是等式的一部分。
撇开 HTTP 不谈,最后一点为什么聊天很重要,与移动设备的工作方式有关,特别是它们的无线电的工作方式。 总而言之,操作收音机是手机中最耗电的部分之一,因此操作系统会一有机会就将其关闭。 启动收音机不仅会耗尽电池电量,还会为每个请求增加更多开销。
TMI(过度获取)
REST 样式服务的下一个问题是发送方式比需要的信息多。 在我们的博客示例中,我们只需要每篇文章的标题和作者姓名,这仅是返回内容的 17% 左右。 对于非常简单的有效载荷,这是 6 倍的损失。 在现实世界的 API 中,这种开销可能是巨大的。 例如,电子商务网站通常将单个产品表示为数千行 JSON。 就像聊天问题一样,REST 服务今天可以使用“稀疏字段集”来有条件地包含或排除部分数据来处理这种情况。 不幸的是,对网络缓存的支持参差不齐、不完整或有问题。
工具和内省
REST API 缺少的最后一件事是自省机制。 如果没有任何关于端点返回类型或结构信息的合同,就无法可靠地生成文档、创建工具或与数据交互。 可以在 REST 中工作以不同程度地解决此问题。 完全实现 OpenAPI、OData 或 JSON API 的项目通常是干净的、指定良好的,并且在不同程度上有良好的文档记录,但这样的后端很少见。 即使是超媒体,一个相对容易实现的成果,尽管几十年来一直在会议演讲中被吹捧,但如果有的话,仍然不经常做得很好。
结论
每种 API 类型都有缺陷,但每种模式都有缺陷。 这篇文章并不是对软件巨头已经奠定的惊人基础的判断,只是对这些模式中的每一个进行冷静的评估,从客户开发人员的角度以它们的“纯粹”形式应用。 我希望你不要离开这种想法,比如 REST 或 RPC 模式被打破,而是思考它们各自是如何做出权衡的,以及工程组织可能集中精力改进自己的 API的领域。
在下一篇文章中,我将探索 GraphQL 以及它如何解决我上面提到的一些问题。 GraphQL 和类似工具的创新在于它们的集成水平,而不是它们的实现。 请,如果您或您的团队不是在寻找“包含电池”的 API,请考虑研究像新的 OpenAPI 规范这样的东西,它可以帮助今天建立更强大的基础!
如果你喜欢这篇文章(或者如果你讨厌这篇文章)并且想给我反馈,请在 Twitter 上以@ebaerbaerbaer 的身份找到我,或者在 ericjbaer 的 LinkedIn 上找到我。