微信号:frontshow

介绍:InfoQ大前端技术社群:囊括前端、移动、Node全栈一线技术,紧跟业界发展步伐。

REST将会过时,而GraphQL则会长存

2018-08-09 17:28 张卫滨 译
作者|Samer Buna
译者|张卫滨
编辑|覃云
在实现 API 时,REST 是常用的方式,但是 RESTful API 有一些固有的不足,在本文中,作者介绍了 GraphQL 的基本原理和设计思想,以及如何借助其灵活性来解决 RESTful API 面临的问题,并且探讨了灵活性所带来的成本 。

在处理过多年的 REST API 之后,当我第一次学习到 GraphQL 以及它试图要解决的问题时,我禁不住发了一条推文,这条推文的内容恰好就是本文的标题。

当然,那个时候,我只是抱着好奇的心态进行了尝试,但现在,我相信当时的戏言却正在变成现实。

请不要误解,我并不是说 GraphQL 将会“杀死”REST 或类似的断定。REST 可能永远都不会消亡,就像 XML 永远也不会灭亡一样。我只是认为 GraphQL 之于 REST,就像 JSON 之于 XML 那样。

本文不会 100% 地鼓吹 GraphQL,这里面会有一个很重要的章节讨论 GraphQL 灵活性的代价。巨大的灵活性会带来巨大的成本。

为何要使用 GraphQL?

GraphQL 能够非常漂亮地解决三个重要的问题:

  • 为了得到视图所需的数据,需要进行多轮的网络调用:借助 GraphQL,要获取所有的初始化数据,我们仅需一次到服务器的网络调用。要在 REST API 中达到相同的目的,我们需要引入非结构化的参数和条件,这是很难管理和扩展的。

  • 客户端对服务端的依赖:借助 GraphQL,客户端会使用一种请求语言,该语言:1)消除了服务器端硬编码数据形式或数量大小的必要性;2)将客户端与服务端解耦。这意味着我们能够独立于服务器端维护和改善客户端。

  • 糟糕的前端开发体验:借助 GraphQL,开发人员只需使用一种声明式的语言表达用户的界面数据需求即可。他们所描述的是需要什么数据,而不是如何得到这些数据。在 GraphQL 中,UI 所需的数据以及开发人员描述数据的方式之间存在紧密的联系。

本文将会详细阐述 GraphQL 是如何解决这些问题的。

在开始之前,有些人可能还不熟悉 GraphQL,所以我们先给出一个简单的定义。

什么是 GraphQL?

GraphQL 就是关于数据通信的。我们有客户端和服务器端,它们之间都需要进行对话。客户端需要告诉服务器端它需要什么数据,而服务器端要以实际的数据满足客户端的需求,GraphQL 就位于这种通信之间。

你可能会问,我们为什么不能让客户端和服务器端直接通信呢?当然可以。

有多个原因促使我们在客户端和服务器端之间放置一个 GraphQL 层。其中有个原因,可能也是最常见的,那就是效率。客户端通常需要跟服务端要求多个资源,而服务端通常只能理解如何响应单个资源。所以,客户端需要发起多轮请求,这样才能收集到它需要的所有数据。

借助 GraphQL,我们可以将这种多请求的复杂性转移到服务端,让 GraphQL 层来对其进行处理。客户端对 GraphQL 层发起一个请求并且会得到一个响应,该响应中精确包含了客户端所需的内容。

使用 GraphQL 还会有很多收益,比如,另外一个收益就是与多个服务进行通信的时候。如果你有多个客户端要从多个服务请求数据的时候,位于中间的 GraphQL 层能够简化和标准化这种通信。尽管这并不是针对 REST API 的(因为它也能很容易地实现),但是 GraphQL 运行时提供了一个结构化和标准化的方式来实现这一点。

客户端不会与两个不同的数据服务直接交互,我们现在可以让客户端与 GraphQL 层进行通信。然后,GraphQL 层会与两个不同的数据服务进行通信。这样的话,GraphQL 首先能够将客户端进行隔离,这样它们就没有必要使用多种语言进行通信了,同时,GraphQL 还会将一个请求转换为针对不同服务的多个请求,这些不同的服务可能会使用不同的语言编写。

让我们假设有三个不同的人,他们使用不同的语言并且具备不同类型的知识。假设你有一个问题,该问题需要组合这三个人的知识才能给出答案。如果你有一个能够说这三门语言的翻译器,那么为你的问题给出答案就会变得很容易。这其实就是 GraphQL 运行时所做的事情。

计算机还没有足够智能来回答任意的问题(至少目前还不可以),所以它们必须要在某些地方遵循一定的算法。这也是我们需要在 GraphQL 运行时上定义模式的原因,客户端会使用该模式。

基本上来讲,模式就是一个能力文档,它包含了客户端可以请求 GraphQL 层的所有问题的列表。在如何使用模式方面有一定的灵活性,因为我们在这里所讨论的是一个节点图。模式主要体现的是 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 服务本身之中,不管客户端实际需要哪些字段,REST API 服务始终都会返回所有的字段。GraphQL 对该问题的描述术语是过度加载(over-fetching)不需要的信息。不管是对于客户端还是对于服务器端,这都是网络和内存资源的一种浪费。

REST API 的另外一个大问题是版本化。如果你需要支持多版本的话,通常意味着要有多个端点。在使用和维护这些端点的时候,这通常会导致更多的问题,而这也可能是服务端出现代码重复的原因所在。

上文所述的 REST API 的问题恰好是 GraphQL 所要致力解决的。上面所述的这些肯定不是 REST API 的所有问题,我也不想过多讨论 REST API 是什么,不是什么。我主要讲的是基于资源的 HTTP 端点 API。这些 API 最终都会变成常规 REST 端点和自定义专门端点的混合品,其中自定义的专门端点大多都是因为性能的原因而制作的。在这种情况下,GraphQL 能够提供好得多的方案。

GraphQL 的魔力是如何实现的?

在 GraphQL 背后有着很多理念和设计决策,但是最为重要的包括:

  • GraphQL 模式是强类型的模式。要创建 GraphQL 模式,我们需要按照类型来定义字段。这些类型可以是原始类型,也可以是自定义类型,模式中的任何内容都需要一个类型。这种丰富的类型系统允许实现丰富特性,比如具备内省功能的 API,以及为客户端和服务端构建强大的工具;

  • GraphQL 将数据以 Graph 的形式来进行表示,而数据很自然的表现形式就是图。如果想要表示任意的数据,那正确的结构就是图。GraphQL 运行时允许我们以图 API 的方式来表示数据,该 API 能够匹配数据的自然图形形状。

  • GraphQL 具有一个声明式的特质来表示数据需求。GraphQL 为客户端提供了一种声明式的语言,允许它们描述其数据需求。这种声明式的特质围绕 GraphQL 语言创建了心智模型,这与我们使用英语思考数据需求的方式非常接近,从而使得 GraphQL API 要比其他替代方案容易得多。

其中,正是由于最后一项理念,我个人认为 GraphQL 将是一个游戏规则的改变者。

这些都是高层级的理念,接下来让我们看一些细节。

为了解决多轮网络调用的问题,GraphQL 将响应服务器变成了只有一个端点。从根本上来讲,GraphQL 将自定义端点的思想发挥到了极致,将整个服务器变成了一个自定义的端点,使其能够应对所有的数据请求。

与这个单端点概念相关的另一个重要理念是富客户端请求语言(rich client request language),这是使用自定义端点所需的。如果没有客户端请求语言的话,单端点是没有什么用处的。它需要有一种语言来处理自定义的请求并为该请求响应数据。

具备客户端请求语言就意味着客户端将会是可控的。客户端能够确切地请求它们想要的内容,服务器端则能够确切地给出客户端想要的东西。这解决了过度加载的问题。

在版本化方面,GraphQL 有一种非常有趣的做法,能够彻底避免版本化的问题。从根本上来讲,我们可以添加新的字段,而不必移除旧的字段,因为我们有一个图,从而可以通过添加节点来灵活地扩展这个图。所以,我们可以为旧 API 继续保留其路径,并引入新的 API,而不必将其标记为新版本。API 只是不断增长而已。

这对于移动端尤为重要,因为我们无法控制它们使用哪个版本的 API。一旦安装之后,移动应用可能会多年一直使用相同版本的旧 API。在 Web 端,我们能够很容易地控制 API 的版本,我们只需推送并使用新的代码即可。对移动应用来说,这样做就有些困难了。

还不完全相信吗?我们通过一个具体的例子来对 GraphQL 和 REST 进行一对一的比较如何?

RESTful API 与 GraphQL API 的样例

假设我们是开发人员,负责构建一个崭新的用户界面,展现《星球大战》电影及其角色。

我们要构建的第一个 UI 界面很简单:显示每个《星球大战》人物信息的视图。例如,Darth Vader 以及他在哪些电影中出现过。这个视图将会展现人物的姓名、出生年份、星球名称以及他们所出现的电影的名字。

听起来非常简单,但实际上我们在处理三种不同类型的资源:人物(Person)、星球(Planet)以及电影(Film)。这些资源之间的关系很简单,任何人都可以猜测数据的形状。每个 Person 对象属于一个 Planet 对象,同时每个 Person 对象有一个或多个 Film 对象。

这个 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 数据对象中所有的“key”。

现在,我们看一下如何使用 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

从服务器端得到这六个响应之后,我们就可以就可以将它们组合起来以满足视图的数据需求。

为了满足一个简单 UI 的数据需求,我们发起了六轮请求,除此之外,我们在这里的方式是命令式的。我们需要给出如何获取数据以及如何处理数据使其满足视图需求的指令。

如果你想要明白我的真实含义的话,那么你可以自行尝试一下。在 http://swapi.co/ 站点上,《星球大战》的数据目前有一个 RESTful API。你可以去那里尝试构建我们的人员数据对象。所使用的 key 可能会略有差异,但是 API 端点是相同的。你需要六次 API 调用,除此之外,在这个过程中还会过度加载视图并不需要的信息。

当然,这仅仅是该数据的一种 RESTful API 实现方式而已。我们可能还会有更好的实现方式,让视图编写起来更加容易。例如,如果 API 服务器的实现能够嵌套资源并理解人员和电影之间的关联关系,那么我们通过该 API 来读取电影数据:

GET - /people/{id}/films

但是,纯粹的 RESTful API 可能并不会实现这些,我们需要要求后端工程师为我们创建这个自定义的端点。这就是 RESTful API 进行扩展的现实:我们只能添加自定义端点来有效满足不断增长的客户端需求。管理这样的自定义端点是非常困难的。

现在,我们再来看一下 GraphQL 的方式。GraphQL 在服务端拥抱了自定义端点的理念,并将其发挥到了极致。服务器只有一个端点,至于通道则无关紧要。如果你通过 HTTP 来实现的话,HTTP 方法当前也是无关紧要的。我们假设有一个通过 HTTP 暴露的 GraphQL 端点,其地址为/graphql

因为想要通过一轮请求就将数据获取到,所以需要有一种方式来向服务器表达完整的数据需求。我们通过一个 GraphQL 查询来实现这一点:

GET or POST - /graphql?query={...}

GraphQL 查询只是一个字符串,但是它需要包含我们所需数据的所有片段。此时,声明式的方式就能发挥作用了。

在中文中,会这样描述我们的数据需求:我们需要一个人员的姓名、出生年份、星球的名字以及所有相关电影的名称。在 GraphQL 中,这会翻译为:

{
  person(ID: ...) {
    name,
    birthYear,
    planet {
      name
    },
    films {
      title
    }
  }
}

再次阅读一下使用中文表达的需求,然后将其与 GraphQL 查询进行对比。你会发现,它们非常接近。现在,对比一下这个 GraphQL 查询和我们开始时所见到的原始 JSON 数据。GraphQL 查询与 JSON 数据的格式完全相同,唯一的差异在于”值(value)“部分。如果我们将其想象为问题和答案的关系,所提出的问题就是将答案语句刨除了答案值。

如果答案语句是:

距离太阳最近的行星是水星。

对该问题进行表述时,一种非常好的方式就是将相同语句的答案部分刨除掉:

距离太阳最近的行星是什么?

同样的关系可以用到 GraphQL 查询中。以 JSON 响应为例,我们将其中的”答案“部分(也就是 JSON 中的值)移除掉,最终就能得到一个 GraphQL 查询,它能够非常恰当地表述 JSON 响应所对应的问题。

现在,对比一下 GraphQL 查询和我们为数据所定义的声明式 React UI。GraphQL 查询中的所有内容都用到了 UI 之中,而 UI 中用到的所有内容也都出现在了 GraphQL 查询中。

这是 GraphQL 非常强大的思想模型。UI 知道它所需要的确切数据,抽取需求相对是非常容易的。生成 GraphQL 是一项非常简单的任务,只需将 UI 所需的数据直接抽取为变量即可。

如果我们将这个模型反过来,它依然非常强大。有一个 GraphQL 查询之后,我们就能知道如何在 UI 中使用它的响应,这是因为查询与响应有着相同的”结构“。我们不需要探查响应就能知道如何使用它,我们甚至不需要任何关于该 API 的文档。它都是内置的。

https://github.com/graphql/swapi-graphql 站点将《星球大战》的数据托管为 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 端点不是公开的,也就是只用于客户端(Web 或移动)的内部使用,那么可以使用白名单的方式,服务器只能执行预选得到许可的查询。客户端可以使用一个唯一的查询标识符,请求服务器执行预先许可的查询。Facebook 似乎采用了这种方式。

在使用 GraphQL 时,另外一个需要考虑的地方就是认证和授权。在 GraphQL 查询解析之前、之后或解析的过程中,我们需要在这方面进行处理吗?

要回答这个问题,我们可以将 GraphQL 视为你自己的后端数据获取逻辑之上的一个 DSL(领域特定语言)。它只是一个分层,我们可以将其放到客户端和实际的数据服务(或多个服务)之间。

我们将认证和授权视为另一个分层。GraphQL 并不会为实际的认证和授权逻辑提供帮助。它也不是做这个的。但是如果你想要将这些分层放到 GraphQL 之后的话,我们可以使用 GraphQL 在客户端和限制逻辑之间传输访问 token。这非常类似于在 RESTful API 认证和授权时,我们所采取的做法。

在客户端数据缓存方面,GraphQL 也面临着更多的挑战。RESTful API 由于其字典(dictionary)的特性,因此更容易进行缓存。对应的地址给出数据,因此我们可以使用这个地址本身作为缓存的 key。

在使用 GraphQL 的时候,我们可以采用类似的基本方法,使用查询的文本作为 key 来缓存它的响应。但是,这种方式是有一定限制的,效率不高,并且会导致数据一致性方面的问题。多个 GraphQL 查询的结果很容易出现重叠,这种基本的缓存机制不能解决重叠的问题。

但是,在这方面有一个很好的解决方案。图查询(Graph Query)意味着图缓存。如果我们将 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 还会对答案进行缓存,后续的问题如果请求相同的资源的话,就可以使用缓存了。

  原文链接

https://medium.freecodecamp.org/rest-apis-are-rest-in-peace-apis-long-live-graphql-d412e559d8e4

 课程推荐


 
前端之巅 更多文章 Angular的这10个特性,你可能不知道 2018年,你需要了解这些React Native组件库 以Vue为例,解释JavaScript的反应性 我们用Flutter重写了一个React Native应用 如何构建无渲染Vue组件?
猜您喜欢 我们的路才刚刚开始...... 知识点归纳(3) 边走边听,如何做好自动化测试?-清晨每日分享-吴老电台 三重保护,Micro Focus助你打赢信息安全保卫战! 比特大陆每月向台积电采购2万16纳米晶片,用于挖矿系统