安息吧 REST API,GraphQL 长存

首发于众成翻译javascript


即便与 REST API 打交道这么多年,当我第一次了解到 GraphQL 和它试图解决的问题时,我仍是禁不住把本文的标题发在了 Twitter 上。前端

请别会错意。我不是在说 GraphQL 会“杀死” REST 或别的相似的东西。REST 可能永远不会消失,就像 XML 从没消失过同样。我只是认为 GraphQL 之于 REST,正如 JSON 之于 XML 那般。java

本篇文章实际上并无100%同意 GraphQL。后文会有一个专门的章节来阐述 GraphQL 的灵活性成本,更高的灵活性意味着更高的成本。git

我喜欢“始终以 WHY 开头”,因此让咱们开始吧。github

摘要:为何咱们须要 GraphQL ?

GraphQL 解决的最重要的3个问题分别是:算法

  • 须要进行屡次往返以获取视图所需的数据:使用 GraphQL,你能够随时经过单次往返服务器获取视图所需的全部初始数据。要使用 REST API 实现相同的功能,咱们须要引入难以管理和扩展的非结构化参数和条件。
  • 客户端依赖于服务端:客户端使用 GraphQL 做为请求语言:(1) 消除了服务器对数据形状或大小进行硬编码的须要,(2) 将客户端与服务端分离。这意味着咱们能够把客户端与服务端分离开来,单独进行维护和改进。
  • 糟糕的前端开发体验:使用 GraphQL,开发人员能够声明式地来表达其用户界面的数据需求。他们声明他们须要什么数据,而不是如何获取它。UI 须要哪些数据,与开发人员在 GraphQL 中声明该数据的方式之间存在紧密的联系。

本文将详细介绍 GraphQL 如何解决全部这些问题。shell

在咱们开始以前,若是你还不熟悉 GraphQL,能够从简单的定义开始。数据库

什么是 GraphQL ?

GraphQL 是一门语言。若是咱们将 GraphQL 嵌入某个软件应用,该应用可以声明式地将任意必需的数据传递给一样使用 GraphQL 的后端数据服务。json

就像一个小孩能够很快学会一门新的语言 - 而成年人则相对没那么容易学会 - 从头开始使用 GraphQL 会比引入 GraphQL 到一个成熟的应用中更容易。后端

要让一个数据服务可以使用 GraphQL,咱们须要实现一个运行时层,并将其暴露给想要与服务端通讯的客户端。将服务器端的这一层看做简单的 GraphQL 语言的翻译器,或者表明数据服务的 GraphQL 代理。GraphQL 不是存储引擎,因此它并非一个独立的解决方案。这就是为何咱们不能仅有一个 GraphQL 的服务器,咱们还须要实现一个翻译运行时。

这个抽象层能够用任意语言编写,它定义了一个通用的基于图形的 schema 来发布它所表明的数据服务的功能。使用 GraphQL 的客户端程序能够经过其功能查询该 schema。这种方法使得客户端与服务端解耦,并容许其二者独立开发和扩展。

GraphQL 请求能够是查询(读取操做)或突变(写入操做)。对于这两种状况,请求都是一个简单的字符串,GraphQL 服务可使用指定格式的数据解释,执行和解析。一般用于移动和 Web 应用的响应格式为 JSON

什么是 GraphQL?(大白话版)

GraphQL 为数据通讯而生。你有一个客户端和一个服务器,它们须要相互通讯。客户端须要告知服务器须要哪些数据,服务器须要用实际的数据来知足客户端的数据需求。GraphQL 是此种通讯方式的中介。

截图来源于个人 Pluralsight 课程 - 使用 GraphQL 构建可扩展的 API。

你可能会问,为何客户端不直接与服务器通讯呢? 固然能够。

在客户端和服务器之间加入 GraphQL 层的考量有多种缘由。其中之一,也许是最受欢迎的缘由即是效率。客户端一般须要向服务器请求多个资源,而服务器会用单个资源进行响应。因此客户端的请求最终会屡次往返服务器,以收集全部须要的数据。

使用 GraphQL,咱们基本上能够将这种多个请求的复杂度转移到服务器端,而且经过 GraphQL 层处理它。客户端向 GraphQL 层发起单个请求,并得到一个彻底符合客户端需求的响应。

引入 GraphQL 层有诸多好处。例如,一大好处即是能与多个服务进行通讯。当你有多个客户端请求多个服务的数据时,中间的 GraphQL 层能够简化和标准化此通讯过程。尽管这并非拿来与 REST API 做比较的一个重点 - 由于这很容易实现,而 GraphQL 运行时提供了一种结构化和标准化的方式。

截图来源于个人 Pluralsight 课程 - 使用 GraphQL 构建可扩展的 API。

咱们可让客户端与 GraphQL 层通讯,而不是直接链接两个不一样的数据服务(如上面的幻灯片中那样)。而后 GraphQL 层将与两个不一样的数据服务进行通讯。GraphQL 首先将客户端从须要与多种语言进行通讯中隔离,并将单个请求转换为使用不一样语言的多个服务的多个请求。

想象一下,有三我的说三种不一样的语言,并拥有不一样的知识类型。而后,只有把全部三我的的知识结合在一块儿才能获得回答。若是你有一个能说这三种语言翻译人员,那么把你的问题的答案结合在一块儿就很容易。这正是 GraphQL 运行时所作的。

计算机还没有聪明到能回答任何问题(至少如今尚未),因此它们必须遵循既定的算法。这就是为何咱们须要在 GraphQL 运行时中定义一个 schema,而且该 schema 能被客户端所使用。

这个 schema 基本能够视为一个功能文档,其中列出了客户端能够请求 GraphQL 层的全部查询。由于咱们在这里使用的是节点的图,因此使用 schema 会带来一些灵活性。该 schema 大体表示了 GraphQL 层能够响应的范围。

还不够清楚?咱们能够说 GraphQL 其实根本就是:REST API 的接替者。因此让我回答一下你最有可能问的问题。

REST API 有什么问题?

REST API 最大的问题是其多端点的本质。这要求客户端进行屡次往返以获取数据。

REST API 一般是端点的集合,其中每一个端点表明一个资源。所以,当客户端须要获取多个资源的数据时,须要对 REST API 进行屡次往返,以将其所需的数据放在一块儿。

在 REST API 中,没有客户端请求语言。客户端没法控制服务器返回的数据。没有任何语言能够这样作。更确切地说,可用于客户端的语言很是有限。

例如,READ REST API 端点多是

  • GET /ResouceName ——从该资源获取全部记录的列表;
  • GET /ResourceName/ResourceID ——获取该 ID 标识的单条记录。

例如,客户端不能指定为该资源中的记录选择哪些字段。这意味着 REST API 服务将始终返回全部字段,而无论客户端实际须要哪些。GraphQL 针对这个问题定义的术语是超量获取不须要的信息。这对客户端和服务器而言都是网络和内存资源的浪费。

REST API 的另外一大问题是版本控制。若是你须要支持多个版本,那一般意味着须要新的端点。而在使用和维护这些端点时会致使诸多问题,而且这可能致使服务器上的代码冗余。

上面提到的 REST API 的问题正是 GraphQL 试图要解决的问题。它们固然不是 REST API 的全部问题,我也不想讨论 REST API 是什么。我主要讨论的是比较流行的基于资源的 HTTP 端点 API。这些 API 中的每个最终都会变成一个具备常规 REST 端点 + 因为性能缘由而制定的自定义特殊端点的组合。这就是为何 GraphQL 提供了更好的选择。

GraphQL如何作到这一点?

GraphQL 背后有不少概念和设计决策,但最重要的多是:

  • GraphQL schema 是强类型 schema。要建立一个 GraphQL schema,咱们要定义具备类型字段。这些类型能够是原语的或者自定义的,而且 schema 中的全部其余类型都须要类型。这种丰富的类型系统带来丰富的功能,如拥有内省 API,并可以为客户端和服务器构建强大的工具。
  • GraphQL 使用图与数据通讯,数据天然是图。若是须要表示任何数据,右侧的结构即是图。GraphQL 运行时容许咱们使用与该数据的天然图形式匹配的图 API 来表示咱们的数据。
  • GraphQL 具备表达数据需求的声明性。GraphQL 为客户端提供了一种声明式语言,以便表达它们的数据需求。这种声明性创造了一个关于使用 GraphQL 语言的内在模型,它接近于咱们用英语考虑数据需求的方式,而且它让使用 GraphQL API 比备选方案(REST API)容易得多。

最后一个概念解释了为何我我的认为 GraphQL 是一个规则颠覆者的缘由。

这些都是高层次的概念。让咱们进一步了解一些细节。

为了解决屡次往返的问题,GraphQL 让响应服务器只是做为一个端点。本质上,GraphQL 将自定义端点的思想运用到极致,即让整个服务器成为一个能够回复全部数据请求的自定义端点。

与单一端点概念相关的另外一大概念是使用该自定义的单个端点所需的富客户端请求语言。没有客户端请求语言,单个端点是没有用的。它须要一种语言来处理自定义请求,并响应该自定义请求的数据。

拥有客户端请求语言意味着客户端将处于控制之中。它们能够明确地请求它们须要什么,服务器将会正确应答它们请求的内容。这解决了超量获取的问题。

对于版本控制,GraphQL 的作法颇有趣。咱们能够彻底避免版本控制。本质上,咱们能够添加新的字段,而不须要删除旧的字段,由于咱们有一个图,而且咱们能够经过添加更多的节点来灵活地扩展图。所以,咱们能够在图上留下旧的 API,并引入新的 API,而不会将其标记为新版本。API 只会增加,而不会有版本。

这对于移动客户端尤为重要,由于咱们没法控制它们正在使用的 API 版本。一旦安装,移动应用可能会持续使用同一个旧版 API 不少年。对于 Web,则很容易控制 API 的版本,由于咱们只需推送新的代码便可。然而对于移动应用,这很难作到。

还不彻底信服?要不咱们用实际的例子来对 GraphQL 和 REST 作个一对一的比较?

RESTful APIs vs GraphQL APIs — 示例

假设咱们是负责构建展现“星球大战”电影和角色的崭新用户界面的开发者。

咱们负责构建的第一个 UI 很简单:显示单个星球大战人物的信息。例如,达斯·维德(Darth Vader),以及该角色参演的全部电影。这个视图须要显示人物的姓名,出生年份,星球名称以及全部他们参演的电影的名称。

就是这么简单,咱们只要处理3种不一样的资源:人物,星球和电影。这些资源之间的关系也很简单,任何人都能猜到这里的数据形状。人物对象从属于一个星球对象,而且具备一个或多个电影对象。

这个 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 表示它的视图的方式:

// 容器组件:
<PersonProfile person={data.person} ></PersonProfile>
// PersonProfile 组件:
Name: {person.name}
Birth Year: {person.birthYear}
Planet: {person.planet.name}
Films: {person.films.map(film => film.title)}

这是一个很简单的例子,虽然咱们对星球大战的观影经验可能有所帮助,但 UI 和数据之间的关系实际上是很是清晰的。UI 使用了咱们假想的 JSON 数据对象中的全部“键”。

如今咱们来看看如何使用 RESTful API 请求这些数据。

咱们须要获取单我的物的信息,而且假定咱们知道该人物的 ID,则 RESTful API 会将该信息暴露为:

GET - /people/{id}

这个请求将返回给咱们该人物的姓名,出身年份和其余有关信息。一个设计良好的 RESTful 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

一旦咱们获取了来自服务器的全部6个响应,咱们即可以将它们组合起来,以知足咱们的视图所需的数据。

除了咱们必须作6次往返以知足一个简单的用户界面的简单数据需求的事实,咱们获取数据的方法是命令式的。咱们给出了如何获取数据以及如何处理它以使其准备好渲染视图的说明。

若是你不明白个人意思,你能够本身动手尝试一下。星球大战数据有一个 RESTful API,目前由 http://swapi.co/ 托管。能够去尝试使用它构建咱们的人物数据对象。数据的键可能有所不一样,可是 API 端点是同样的。你须要执行6次 API 调用。此外,你将不得不超量获取视图不须要的信息。

固然,这只是 RESTful API 对于此数据的一个实现。可能会有更好的实现,能使这个视图更容易实现。例如,若是 API 服务器实现了资源嵌套,而且代表了人物与电影之间的关系,则咱们能够经过如下方式读取电影数据:

GET - /people/{id}/films

然而,一个纯粹的 RESTful API 服务器极可能不会像这般实现,而且咱们须要让咱们的后端工程师为咱们额外建立这个自定义的端点。这就是扩展 RESTful 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 API 托管在 https://github.com/graphql/swapi-graphql。能够去尝试使用它构建咱们的人物数据对象。后续咱们探讨的 API 可能会有一些细微的变更,但下面是你可使用这个 API 来查看咱们对视图数据请求的正式查询(以Darth Vader为例):

{
  person(personID: 4) {
    name,
    birthYear,
    homeworld {
      name
    },
    filmConnection {
      films {
        title
      }
    }
  }
}

这个请求定义了一个很是接近视图的响应结构,记住,咱们是在一次往返中得到的全部这些数据。

GraphQL 灵活性的代价

完美的解决方案实际并不存在。因为 GraphQL 过于灵活,将会带来一些明确的问题和担心。

GraphQL 易致使的一个重要威胁是资源耗尽攻击(亦称为拒绝服务攻击)。GraphQL 服务器可能会受到超复杂查询的攻击,这将耗尽服务器的全部资源。查询深度嵌套关系(用户 -> 朋友 -> 朋友...),或者使用字段别名屡次查询相同的字段很是容易。资源耗尽攻击并非特定于 GraphQL 的场景,可是在使用 GraphQL 时,咱们必须格外当心。

咱们能够在这里作一些缓和措施。好比,咱们能够提早对查询进行成本分析,并对可使用的数据量实施某种限制。咱们也能够设置超时时间来终结须要过长时间解析的请求。此外,因为 GraphQL 只是一个解析层,咱们能够在 GraphQL 下的更底层处理速率限制。

若是咱们试图保护的 GraphQL API 端点并不公开,而是为了供咱们本身的客户端(网络或移动设备)内部使用,那么咱们可使用白名单方法和预先批准服务器能够执行的查询。客户端能够要求服务器只执行使用查询惟一标识符预先批准的查询。听说 Facebook 采用的就是这种方法。

认证和受权是在使用 GraphQL 时须要考虑的其余问题。咱们是在 GraphQL 解析过程以前,以后仍是之间处理它们?

为了解答这个问题,你能够将 GraphQL 视为在你本身的后端数据获取逻辑之上的 DSL(领域特定语言)。咱们只需把它看成能够在客户端和咱们的实际数据服务(或多个服务)之间放置的一个中间层。

而后将认证和受权视为另外一层。GraphQL 在实际的身份验证或受权逻辑的实现中并没有用处,由于它的意义并不在于此。可是,若是咱们想将这些层放置于 GraphQL 以后,咱们可使用 GraphQL 来传递客户端和强逻辑之间的访问令牌。这与咱们经过 RESTful API 进行认证和受权的方式很是类似。

GraphQL 另外一项更具挑战性的任务是客户端的数据缓存。RESTful API 因为其字典性质而更容易缓存。特定地址标识特定数据。咱们可使用地址自己做为缓存键。

使用 GraphQL,咱们能够采起相似的基本方式,将查询文本用做缓存其响应的键。可是这种方式有着诸多限制,并且不是颇有效率,而且可能致使数据一致性的问题。多个 GraphQL 查询的结果很容易重叠,而这种基础的缓存方式没法解决重叠的问题。

对于这个问题有一个很巧妙的解决方案,那就是使用图查询表示图缓存。若是咱们将 GraphQL 查询响应范式化为一个扁平的记录集合,给每条记录一个全局惟一的 ID,那么咱们就能够缓存这些记录,而不是缓存完整的响应。

然而这不是一个简单的过程。记录将会相互引用,咱们将在其中管理循环图。操做和读取缓存须要遍历查询。尽管咱们须要编写一个中间层来处理这些缓存逻辑,可是这种方式整体上比基于响应的缓存更有效率。Relay.js 即是一个采用这种缓存策略并在内部实现自动管理的框架。

对于 GraphQL,或许咱们应该关心的最重要的问题是一般被称为 N+1 SQL 查询的问题。GraphQL 查询字段被设计为独立的功能,而且使用数据库中的数据解析这些字段可能会致使对已解析字段产生新的数据库请求。

对于简单的 RESTful API 端点逻辑,能够经过加强结构化的 SQL 查询来分析,检测和解决 N+1 问题。对于 GraphQL 动态解析的字段,就没那么简单了。好在 Facebook 开创了一个可行的解决方案:DataLoader

顾名思义,DataLoader 是一个可用于从数据库读取数据并使其可用于 GraphQL 解析函数的工具程序。咱们可使用 DataLoader 而不是直接使用 SQL 查询从数据库中读取数据,而 DataLoader 将做为咱们的代理,以减小咱们发送到数据库的实际 SQL 查询。

DataLoader 的原理是使用批处理和缓存的组合。若是相同的客户端请求致使须要向数据库请求多个数据,则可使用 DataLoader 来合并这些请求,并从数据库批量加载其响应。DataLoader 还将缓存响应以使其可用于相同资源的后续请求。

谢谢阅读!

相关文章
相关标签/搜索