[译] REST API 已死,GraphQL 长存

REST API 已死,GraphQL 长存

在使用多年的 REST API 后,当我第一次接触到 GraphQL 并了解到它试图解决的问题时,我没法抗拒给本文取了这样一个标题。html

https://twitter.com/samerbuna/status/644548922979954688

固然,过去,这可能只是本人有趣的尝试,可是如今,我相信这有趣的预测正在慢慢发生。前端

请不要理解错了,我并无说 GraphQL 会干掉 REST 或其它相似的话语,REST 大概永远不会真正消亡,就像 XML 并不会真正消亡同样。我只是认为 GraphQL 与 REST 的关系将会变得像 JSON 与 XML 同样。node

本文并非百分百支持 GraphQL。须要注意 GraphQL 灵活性所带来的开销。好的灵活性经常伴随着大的开销。react

我信仰"一切从提问开始",让咱们开始吧。android

总而言之:为何须要 GraphQL?

GraphQL 漂亮地解决了以下三个重要问题:ios

  • 填充一个视图须要的数据进行屡次往返拉取: 使用 GraphQL,咱们总可以经过一次往返就能从服务器获取到用来填充视图的全部初始化数据。若是使用 REST API,要到达相同的效果,咱们须要引入非结构化的参数和条件,使管理和维护变得困难。
  • 客户端对服务端产生依赖: 使用 GraphQL,客户端就有了本身的语言:1) 无需服务端对数据的结构和规格进行硬编码 2) 客户端与服务端解耦。这意味着咱们能让客户端与服务器分离并单独对它进行维护和升级。
  • 糟糕的前端开发体验: 使用 GraphQL,前端开发人员使用声明式语言表达其对填充用户界面所须要的数据的需求。他们表达他们须要什么,而不是如何使其可用。这样就在 UI 须要的数据和开发人员在 GraphQL 中表述的数据之间构建一种紧密的联系。

本文将就 GraphQL 如何解决这些问题进行详细阐述。git

在咱们正式开始以前,考虑到你目前可能还不熟悉 GraphQL ,咱们先从简单定义开始。github

GraphQL 是什么?

GraphQL 是一门语言。 若是咱们传授 GraphQL 语言给一款应用,这款应用就可以向支持 GraphQL 的后端数据服务声明式传达数据需求。web

就像小孩子很快就能学会一种新语言,而成年人却很难学会同样,使用 GraphQL 从头开始编写应用比将 GraphQL 添加到一款成熟的应用要容易不少。算法

为了让数据服务支持 GraphQL,咱们须要实现一个运行时层并将它暴露给想要与服务通讯的客户端。能够将这个添加到服务端的层简单地看做是一位 GraphQL 语言翻译员,或表明数据服务并会说 GraphQL 语言的代理。GraphQL 并非一个存储引擎,因此它不能做为一个独立的解决方案。这就是咱们不能有一个纯粹的 GraphQL 服务,而须要实现一个翻译运行时的缘由。

这个层能够用任何语言编写,它定义了一个通用的基于图的模板来发布它所表明的数据服务的功能。支持 GraphQL 的客户端能够在功能容许的范围内使用这种模版进行查询。这一策略能够将客户端与服务端分离,容许二者独立开发和扩展。

一个 GraphQL 请求既能够是查询(读操做),也能够是修改(写操做)。无论是何种情形,请求均只是一个带有特定格式的简单字符串,GraphQL 服务器能够对其进行解析、执行、处理。在移动和 Web 应用中最多见的响应格式是 JSON

什么是 GraphQL ?(把我当五岁小孩后再向我解释版)

GraphQL 一切为了数据通讯。你有一个须要须要彼此通讯的客户端和服务器,客户端须要告诉服务器它须要什么数据,服务器须要根据客户端的需求返回具体的数据,GraphQL 做为这种通讯的中间人。

屏幕截图中是个人 Pluralsight 课程 —— 使用 GraphQL 构建可扩展 API 你问,客户端难道不能直接与服务器通讯吗?答案是能。

这儿有几个缘由致使咱们须要在客户端和服务器间添加一个 GraphQL 层。缘由之一,可能也是最主要的缘由,这样作更高效。客户端一般须要从服务器获取多个资源,而服务器一般只能理解如何对单个资源进行回复。这就形成客户端最后须要屡次往返服务器才能集齐须要的数据。

经过 GraphQL,咱们基本上能够将这种复杂的屡次请求转移到服务端,让 GraphQL 层来处理。客户端向 GraphQL 层发起单个请求,并获得一个彻底符合客户端需求的响应。

使用 GraphQL 层还有不少其它好处。例如,另外一个大的好处是与多个服务进行通讯。当您有多个客户端向多个服务请求数据时,中间的 GraphQL 可让通讯简化、标准化。尽管与 REST API 比起来这不算是卖点 —— 由于 REST API 也能够很容易地完成一样的工做 —— 但 GraphQL 运行时提供了一种结构化和标准化的方法。

屏幕截图中是个人 Pluralsight 课程 —— 使用 GraphQL 构建可扩展 API 不是让客户端直接请求两个不一样的数据服务(如幻灯片所示),而是让客户端先与 GraphQL 层通讯。GraphQL 层再分别与两个不一样的数据服务通讯。经过这种方式,GraphQL 解决了客户端必须与多个不一样语言的后端进行通讯的问题,并将单个请求转换为使用不一样语言的多个服务的多个请求。

想象一下,你认识三我的,他们说不一样的语言,掌握着不一样领域的知识。而后再想象一下,你遇到一个只有结合三我的的知识才能回答的问题。若是你有一个会说这三种语言的翻译人员,那么任务就变成将你的问题的答案放在一块儿,这就很容易了。这就是 GraphQL 运行时要作的。

计算机尚未聪明到能回答任何问题(至少目前是这样),因此它们必须遵照某种算法。这就是为何咱们须要在 GraphQL 运行时中定义一个模板让客户端来使用的缘由。

这个模板基本上是一个功能文档,它列出了客户端能向 GraphQL 层查询的所有问题。由于模板采用了图形节点因此在使用上具备必定的灵活性。模板也代表了 GraphQL 层能解答哪些问题,不能解答哪些问题。

仍是不理解?让我用最确切最简短的话语来描述 GraphQL :一种 REST API 的替代。接下来让我回答一下你极可能会问的问题。

REST API 有什么错?

REST API 最大的问题是其自然倾向多端点。这形成客户端须要屡次往返获取数据。

REST API 一般由多个端点组成,每一个端点表明一种资源。所以,当客户端须要多个资源时,它须要向 REST API 发起多个请求,才能获取到所须要的数据。

在 REST API 中,是没有描述客户端请求的语言的。客户端没法控制服务器返回哪些数据。没有让客户端对返回数据进行控制的语言。更确切的说,客户端能使用的语言是颇有限的。

例如,有以下进行读取操做的 REST API:

  • GET /ResouceName - 从该资源获取包含全部记录的列表
  • GET /ResourceName/ResourceID - 经过 ID 获取某条特定记录

例如,客户端是不可以指定从该资源的记录中选择哪些字段的。信息仅存在于提供 REST API 的服务中,该服务将始终返回全部字段,而无论客户端须要什么。借用 GraphQL 术语描述这个问题:超额获取(over-fetching) 没用的信息。这浪费了服务器和客户端的网络内存资源 * REST API 的另外一个大问题就是版本控制了。若是你须要支持多版本,那你就须要为此建立多个新的端点。这会致使这些端点很难使用和维护,此外,还形成服务端出现不少冗余代码。

上面列出的一些 REST API 带来的问题都是 GraphQL 试图解决的。这并非 REST API 带来的所有问题,我也不打算说明 REST API 是什么不是什么。我只是在谈论一种最流行的基于资源的 HTTP 终点 API。这些 API 最终都会变成一种具备常规 REST 特性的端点和出于性能缘由定制的特殊端点的组合。

GraphQL 如何实现其魔力?

在 GraphQL 背后有不少的概念和设计策略,这儿列举了一些最重要的:

  • GraphQL 模板是强类型的。要建立一套 GraphQL 模板,咱们须要定义了一些带有类型字段。这些类型能够是原始数据类型也能够是自定义的,在模板中一切均须要类型。丰富的类型系统带来了丰富的特性,如 API 自证,这让咱们可以为客户端和服务端建立强大的工具。
  • GraphQL 以图的形式组织数据,数据天然造成图。若是你须要一个结构描述数据,图是一种不错的选择。GraphQL 运行时让咱们可以使用与该数据的天然图结构匹配的图 API 来表示咱们的数据。 -GraphQL 具备表达数据需求声明性质。GraphQL 让客户端可以以一种声明性的语言描述其对数据的需求。这种声明性带来了一种围绕着 GraphQL 语言使用的心智模型,该模型与咱们用天然语言思考数据需求的方式接近,让咱们使用 GraphQL 时比使用其它方式更容易。

最后一个概念是我为何认为 GraphQL 是游戏规则改变者的缘由。

这些全是抽象概念。让咱们深刻到细节中。

为了解决屡次往返请求的问题,GraphQL 让响应服务器变成一个端点。本质上,GraphQL 把自定义端点这一思想发挥到了极致,它让这个端点可以回复全部数据问题。

伴随着单个端点这一律念的另外一个重要概念是须要一种强大的客户端请求描述语言与自定义的单个端点进行通讯。缺乏客户端请求描述语言,单个端点是没有意义的。它须要一种语言解析自定义请求以及根据自定义请求返回数据。

拥有一门客户端请求描述语言意味这客户端可以对请求进行控制。客户端可以精确表达它们须要什么,服务端也能精准回复客户端须要的。这就解决了超额获取的问题。

当涉及到版本时,GraphQL 提供了一种有趣的解决方式。版本可以被彻底避免。基本上,咱们只须要在保留老的字段的基础上添加新字段便可,由于咱们用的是图,咱们能很灵活的在图上添加更多节点。所以,咱们能够在图上留下旧的 API,并引入新的 API,而不会将其标记为新版本。API 只是多了更多节点。

这点对于移动端尤其重用,由于咱们没法充值这些移动端使用的版本。一经安装,移动端应用可能数年都使用老版本 API 。对于 Web,咱们能够经过发布新代码简单的控制 API 版本,对于移动端应用,这点很难作到。

尚未彻底相信? 结合实例一对一对比 GraphQL 和 REST 怎么样?

REST 风格 API vs GraphQL API —— 案例

咱们假设咱们是开发者,负责构建闪亮全新的用户界面,用来展现星球大战影片和角色。

咱们要构建的第一份 UI 很简单:一个显示单个星球大战角色的信息视图。例如,达斯·维德以及电影中出场的其余角色。这个视图须要显示角色的姓名、出生年份、母星名、以及出场的全部影片中出现的头衔。

听起来很简单,咱们实际上已经须要处理三种不一样的资源:人物、星球和电影。资源之间的关系很简单,任何人都很容易就猜出这里的数据组成。

此 UI 的 JSON 数据可能相似于:

{
  "data": {
    "person": {
      "name": "Darth Vader",
      "birthYear": "41.9BBY",
      "planet": {
        "name": "Tatooine"
      },
      "films": [
        { "title": "A New Hope" },
        { "title": "The Empire Strikes Back" },
        { "title": "Return of the Jedi" },
        { "title": "Revenge of the Sith" }
      ]
    }
  }
}
复制代码

假设数据服务按照上面的结构返回数据给咱们。咱们有一种可行的方式即便用 React.js 来展示视图:

// The Container Component:
<PersonProfile person={data.person} ></PersonProfile>

// The PersonProfile Component:
Name: {person.name}
Birth Year: {person.birthYear}
Planet: {person.planet.name}
Films: {person.films.map(film => film.title)}
复制代码

这是一个简单例子,此外咱们关于星球大战的经验也能帮咱们一点忙,咱们能够很清楚的明白 UI 和数据之间的关系。与咱们想象一致,UI 是使用了 JSON 数据对象中的所有的键。

让咱们来看看如何经过 REST 风格 API 获取这些数据。

咱们须要单个角色的信息,假设咱们知道这个角色的 ID,REST 风格的 API 倾向于这样输出这些信息:

GET - /people/{id}
复制代码

这个请求将会返回角色的姓名、出生年份以及一些其它信息给咱们。一个规范的 REST 风格 API 将会返回给咱们角色星球的 ID 以及该角色出现过的全部影片的 ID 组成的数组。

这个请求以 JSON 格式返回的响应相似于:

{
  "name": "Darth Vader",
  "birthYear": "41.9BBY",
  "planetId": 1
  "filmIds": [1, 2, 3, 6],
  *** 其它信息咱们不须要 ***
}
复制代码

而后为了获取星球名称,咱们发起请求:

GET - /planets/1
复制代码

接着为了获取影片中的头衔,咱们发起请求:

GET - /films/1
GET - /films/2
GET - /films/3
GET - /films/6
复制代码

当从服务器接受到全部的六个数据后,咱们才能将其组合并生成知足视图须要的数据。

除了有须要六次往返才能获取到知足一个简单 UI 需求的数据这一事实外,这种方式并没有不可。咱们阐明了如何获取数据,以及如何处理数据使其知足视图须要。

若是你想确认我说的你能够本身动手尝试。有一个部署在 swapi.co/ 上的 REST API 服务提供了星球大战的数据,点进去,在里面尝试构造角色数据。数据的键名可能不一样,但 API 端点是一致的。你一样须要进行六次 API 调用。一样,你不得不超额获取视图不须要的信息。

固然,这只是 REST API 的一个实现方式,可能有更好的实现让生成视图更简单。例如,若是 API 服务支持资源嵌套并能理解角色和影片之间的关系,咱们可以经过这种方式获取影片数据:

GET - /people/{id}/films
复制代码

然而,一个纯粹的 REST API 服务很难实现这点。咱们须要让后端工程师为咱们建立自定义端点。这形成 REST API 规模不断增加这一事实 —— 为了知足不断增加的客户端的须要,咱们不断添加自定义端点。管理这些自定义端点很难。

让咱们来看一看 GraphQL 策略。GraphQL 在服务端拥抱自定义端点思想并把它发展到极致。服务将只是一个端点,通道变得没有意义。若是咱们使用 HTTP 实现,HTTP 方法将失去意义。假设咱们有一个单一的 GraphQL 端点,它的 HTTP 地址是 /graphql

由于咱们但愿一次往返获取须要的数据,因此咱们须要明明白白告诉服务器咱们须要哪些数据。咱们经过 GraphQL 进行查询:

GET or POST - /graphql?query={...}
复制代码

GraphQL 查询只是字符串,但它将包含咱们须要的所有数据。这就是声明的强大之处。

英语中,咱们这样阐述数据需求:咱们须要角色名、出生年份、星球名和在全部出现过的影片中的头衔。经过 GraphQL,咱们进行以下转换:

{
  person(ID: ...) {
    name,
    birthYear,
    planet {
      name
    },
    films {
      title
    }
  }
}
复制代码

再细读一次英语表述的需求并与 GraphQL 查询进行对比。它们不能再更接近了。如今,将 GraphQL 查询与咱们最开始用到的原始 JSON 数据进行对比。GraphQL 查询彻底与 JSON 数据结构相对应,不过排除全部是值的部分。若是咱们仿照问题与答案关系来考虑这中状况,那问题就是没有具体答案的答案原语。

若是答案是:

离太阳最近的星球是水星。

一种好的提问方式是保留原话只去掉提问部分:

哪一个星球里太阳最近?

这种关系一样适用于 GraphQL 查询。拿着 JSON 格式的响应数据,移除全部是答案的部分(做为值的对象),最后你获得了一个很是适合表明关于 JSON 响应问题的 GraphQL 查询。

如今,将 GraphQL 查询和与咱们展现数据的声明性 React UI 对比。全部出如今 GraphQL 查询中的数据都出如今了 UI 中。全部出如今 UI 中的数据都出如今了 GraphQL 查询中。

这就是 GraphQL 强大的心智模型。UI 知晓它所须要的确切数据,提取须要的数据也很容易。编写 GraphQL 查询变成一个从 UI 中提取做为变量这一简单的工做。

将模型进行反转,它仍然很强大。若是咱们知道了 GraphQL 查询,咱们一样知道如何在 UI 中使用相应数据。咱们不须要分析响应数据就能使用它,也不须要的这些 API 的文档。这一切都是内建的。

获取星球大战数据的 GraphQL 托管在 github.com/graphql/swa…。点击进去并尝试构造角色数据。只有一点点不一样,咱们以后会谈论,如下是能够从这个 API 中获取视图所须要数据的正式查询(使用达斯·维德举例)

{
  person(personID: 4) {
    name,
    birthYear,
    homeworld {
      name
    },
    filmConnection {
      films {
        title
      }
    }
  }
}
复制代码

这个请求返回的咱们的响应数据结构十分接近视图用到的,记住,这些数据是咱们经过一次往返得到的。

GraphQL 灵活性带来的开销

完美的解决方案是不存在的。GraphQL 带来了灵活性,也带来了一些明确的问题和考量。

GraphQL更容易的形成一个安全隐患是资源耗尽型攻击(拒绝服务攻击)。GraphQL 服务器可能会受到伴随着极其复杂的查询的攻击,形成服务器资源耗尽。很容易就能构造一个深度嵌套关系链(用户 -> 好友 -> 好友的好友。) 或者屡次经过字段别名请求同一字段的查询。资源耗尽型攻击并无限定 GraphQL,可是在使用 GraphQL 时,咱们要特别当心。

这儿有一些缓解措施咱们能够用上。咱们能够进行一些高级查询的开销分析,对单个用户请求的数据量作某种限制。咱们也能够实现一种机制对须要很长时间处理的请求进行超时处理。此外,考虑到 GraphQL 就只是一个处理层,咱们能在 GraphQL 之下的更底层进行速率限制。

若是咱们尝试保护的 GraphQL API 端点并非公开的,仅供咱们私有的客户端(web、移动)内部访问,咱们可以使用白名单策略并预先审核服务器可以处理的查询。客户端仅能经过惟一查询标识码向服务器发起审核过的查询。Facebook 彷佛就采用了这种策略。

当使用 GraphQL 时,咱们还须要考虑到认证和受权。咱们是在 GraphQL 解析请求以前,以后仍是之间处理它们呢?

为了回答这个问题,须要将 GraphQL 想象成你一种位于你的后端数据请求逻辑顶层的 DSL(领域限定语言)。它只是一个可以被咱们放在客户端与实际数据服务(多个)之间的处理层。

将认证和受权当成另外一个处理层。GraphQL 与认证和受权逻辑的具体实现关系不大。它的意义不在这儿。可是若是咱们把这些层放在 GraphQL 以后,咱们就能够在 GraphQL 层使用访问令牌连通客户端与执行逻辑。这和咱们在 REST 风格 API 处理认证和受权相似。

另外一件由于 GraphQL 而变得更具挑战性的任务是客户端数据缓存。REST 风格的 API 因其相似目录更容易进行缓存处理。REST API 经过访问路径获取数据,咱们可以使用访问路径做缓存键。

对于 GraphQL,咱们可以采用相似的策略使用查询字段做为响应数据的缓存键。可是这种方式有限制,效率低下,还容易形成数据一致性方面的问题。缘由是多个 GraphQL 查询的结果很容易重叠,而这种缓存策略并无考虑到这种重叠。

这个问题有一个很好的解决方案。一个图的查询意味这一个图的缓存。若是咱们将一个 GraphQL 查询的响应数据正则化为一个平铺的记录集合,为每一个记录设置一个全局惟一 ID,咱们就可以只缓存这些记录而不用缓存整个响应了。

这种处理并不容易。这样致使一些记录指向另外一些记录,致使咱们可能得管理一个环形图,致使在写入和读取缓存时咱们须要进行遍历,致使咱们须要编写一个层来处理缓存逻辑。可是,这种方法整体上比基于响应的缓存更高效。Relay.js 就是一个采用这种缓存策略并在内部进行自动管理的框架。

对于 GraphQL 咱们最须要关心的问题多是被广泛称做 N+1 SQL 查询的问题了。GraphQL 的字段查询被设计成独立的函数,从数据库获取这些字段可能形成每一个字段都须要一个数据库查询。

简单 REST 风格 API 端点的逻辑,易分析,易检测,能够优化 SQL 查询语句来解决 N+1 问题。而 GraphQL 须要动态处理字段,这点不容易作到。幸运的是 Facebook 正在研发一个处理相似问题的可能的解决方案:DataLoader。

如名字暗示,DataLoader 是一款能让咱们从数据库读取数据并让数据能被 GraphQL 处理函数使用的工具。咱们使用 DataLoader,而不是直接经过 SQL 查询从数据库获取数据,将 DataLoader 做为代理以减小咱们实际须要发送给数据库的 SQL 查询。

DataLoader 使用批处理和缓存的组合来实现。若是同一个客户端请求会形成屡次请求数据库,DataLoader 会整合这些问题并从数据库批量拉取请求数据。DataLoader 会同时缓存这些数据,当有后续请求须要一样资源时能够直接从缓存获取到。


谢谢你阅读本文。若是你以为本文有用,点击下面的链接。关注我以获取更多的关于 Node.js 和 JavaScript 的文章。

我在 Pluralsight and Lynda 上建立了在线课程。我最近的课程包含 Advanced React.js](www.pluralsight.com/courses/rea…), Advanced Node.js, and Learning Full-stack JavaScript

我还在作让 JavaScript、Node.js、React.js 和 GraphQL 初学者进阶到更高级别的线上线下培训。若是您正在寻找教练,请与我联系。若是你您对本文以及我写的其它文章有疑问,能够在 这个slack帐户(你能够邀请本身) 找到我并在 #questions 频道提问。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索