Graphql 是用于 api 的查询语言,也是用于使用现有数据完成这些查询的运行时。 Graphql 为您的 API 中的数据提供了一个完整的、易于理解的描述,并且为客户提供了要求他们所需要的东西的能力,仅此而已。

随着时间的推移,它简化了不断发展的 api,并支持强大的开发工具。 在本指南中,我们将看到 GraphQL 的优点和缺点,这样您就可以自己决定它是否适合您的项目。

精确的数据获取

无论如何强调 GraphQL 精确数据获取特性的重要性和有用性都不为过。 使用 GraphQL,您可以向您的 API 发送一个查询,然后得到您所需要的东西,不多也不少。 就是这么简单。 如果您将这个特性与 REST 的传统直觉特性进行比较,就会明白这是对我们最初做事方式的一个重大改进。

Graphql 通过根据客户端应用程序的需要对数据进行选择,最大限度地减少了通过网络传输的数据量。 因此,移动客户端可以获取更少的信息,因为与 web 应用程序的大屏幕相比,它可能不需要在小屏幕上显示。

因此,与返回固定数据结构的多个端点不同,GraphQL 服务器只公开单个端点,并精确地响应客户机请求的数据。

考虑这样一种情况,您希望调用一个拥有两个资源的 API 端点,即艺术家和他们的轨道。 为了能够请求一个特定的艺术家或他们的音乐曲目,你将有一个 API 结构如下:

METHOD /api/:resource:/:id:

对于传统的 REST 模式,如果我们想使用提供的 API 查找每个艺术家的列表,我们必须向根资源端点发出一个 GET 请求,如下所示:

GET /api/artists

如果我们想从艺术家列表中查询某个艺术家,该怎么办? 然后我们必须将资源 ID 附加到端点,如下所示:

GET /api/artists/1

本质上,我们必须调用两个不同的端点来获取所需的数据。 使用 GraphQL,每个请求都可以在一个端点上执行,执行的操作和返回的数据都是在查询本身中定义的。 假设我们想要获得一个 artisits track 和 duration,使用 GraphQL,我们会有一个如下的查询:

GET /api?query={ artists(id:"1") { track, duration } }

这个查询指示 API 查找 ID 为1的艺术家,然后返回其轨道和持续时间,这正是我们想要的,不多也不少。 这个相同的端点也可以用于在 API 中执行操作。

一个请求,许多资源

Graphql 的另一个有用的特性是,它使得通过单个请求获取所有需要的数据变得简单。 Graphql 服务器的结构使得以声明方式获取数据成为可能,因为它只公开一个端点。

考虑这样一种情况,用户希望请求获得特定艺术家的详细信息,例如(姓名、 id、曲目等)。 对于传统的 REST 直观模式,这将需要至少两个对两个端点 / 艺术家和 / 轨道的请求。 然而,使用 GraphQL,我们可以定义查询中需要的所有数据,如下所示:

// the query request

artists(id: "1") {
  id
  name
  avatarUrl
  tracks(limit: 2) {
    name
    urlSlug
  }
}

在这里,我们定义了一个单一的 GraphQL 查询来请求多个资源(艺术家和音轨)。 这个查询将返回所有并且只返回请求的资源,如下所示:

// the query result
{
  "data": {
    "artists": {
      "id": "1",
      "name": "Michael Jackson",
      "avatarUrl": "https://artistsdb.com/artist/1",
      "tracks": [
        {
          "name": "Heal the world",
          "urlSlug": "heal-the-world"
        },
        {
          "name": "Thriller",
          "urlSlug": "thriller"
        }
      ]
    }
  }
}

从上面的响应数据可以看出,我们已经通过单个 API 调用获取了 / artists 和 / tracks 的资源。 这是 GraphQL 提供的一个强大的特性。 正如您已经想到的,这个特性对于高度声明性的 API 结构的应用程序是无限的。

现代的兼容性

现代应用程序以综合的方式构建,单个后端应用程序提供运行多个客户机所需的数据。 网络应用程序,移动应用程序,智能屏幕,手表等,现在只能依靠一个单一的后端应用程序的数据功能有效。

Graphql 支持这些新趋势,因为它可以用于连接后端应用程序,并满足每个客户端的需求(嵌套的数据关系,只获取所需的数据,网络使用要求等) ,而不需要为每个客户端分配单独的 API。

大多数情况下,要做到这一点,后端将被分解为具有不同功能的多个微服务。 通过这种方式,我们可以很容易地通过模式拼接为微服务提供特定的功能。 模式拼接使得从不同的模式创建单个一般模式成为可能。 因此,每个微服务都可以定义自己的 GraphQL 模式。

然后,可以使用模式拼接将所有单个模式编织到一个通用模式中,然后每个客户端应用程序都可以访问该通用模式。 最后,每个微服务都可以有自己的 GraphQL 端点,而一个 GraphQL API 网关将所有模式合并到一个全局模式中,使其对客户机应用程序可用。

为了演示模式拼接,让我们考虑 Sakho Stubailo 使用的相同情况,同时解释我们有两个相关 api 的拼接。 Ticketmaster 的宇宙事件管理系统的新公共 GraphQL API 和 Launchpad 的 Dark Sky 天气 API,由 Matt Dionis 创建。 让我们来看看可以分别针对这些 api 运行的两个查询。 首先,使用 Universe API,我们可以获得特定事件 ID 的详细信息: 使用 Dark sky weather API,我们可以获得同一地点的详细信息,如下所示:

现在有了 GraphQL 模式拼接,我们可以做一个操作来合并这两个模式,这样我们就可以很容易地将这两个查询并排发送:

很棒,不是吗。 你可以通过 Sashko Stubailo 对 GraphQL 模式的拼接进行深入研究,以便更深入地理解所涉及的概念。 通过这种方式,GraphQL 可以将不同的模式合并到一个通用模式中,所有客户机都可以从中获得资源,从而轻松地采用新的现代开发风格。

字段级别的弃用

这是一个 GraphQL 特性,它给我带来了乐趣。 作为开发人员,我们习惯于调用不同版本的 API,通常会得到非常奇怪的响应。 传统上,当我们对资源或者我们现有资源的结构进行了更改时,我们就会使用 API 版本,因此我们需要反对并发展一个新的版本。

例如,我们可以拥有一个类似于 API.domain.com/resources/v1的 API,在接下来的几个月或几年中的某个时间点,可能会发生一些变化,资源或者资源的结构也会发生变化,因此,接下来最好的事情就是将这个 API 发展成 API.domain.com/resources/v2应用程序接口来捕捉最近的所有变化。

此时,v1中的一些资源将被弃用(或者在用户迁移到新版本之前暂时处于活动状态) ,并且在收到对这些资源的请求时,将得到诸如弃用通知之类的意外响应。

在 GraphQL 中,可以在字段级别反对 API。 当不推荐使用某个特定字段时,客户机在查询该字段时会收到一个不推荐使用的警告。 过一段时间后,当不再有很多客户机使用该模式时,可能会从该模式中删除已弃用的字段。

因此,不必完全对 API 进行版本控制,而是可以随着时间的推移逐步改进 API,而不必重新构造整个 API 模式。

缓存

缓存是数据的存储,以便未来对该数据的请求能够得到更快的服务; 缓存中存储的数据可能是早期计算的结果,或者是存储在其他地方的数据的重复。 缓存 API 响应的目的主要是为了更快地从未来的请求中获得响应。 与 GraphQL 不同,缓存被构建到了 RESTful api 能够利用的 HTTP 规范中。

使用 REST,您可以使用 URL 访问资源,因此您可以在资源级别上缓存,因为您将资源 URL 作为标识符。 在 GraphQL 中,这变得很复杂,因为每个查询可能不同,即使它操作的是同一个实体。

在一个查询中,您可能只对艺术家的名字感兴趣,但是在下一个查询中,您可能希望获得艺术家的歌曲和发行日期。 这是缓存最复杂的地方,因为它需要字段级别的缓存,这对于 GraphQL 来说不是一件容易的事情,因为它只使用一个端点。

尽管如此,GraphQL 社区认识到了这个困难,并且一直在努力使得对 GraphQL 用户的缓存更加容易。 像 Prisma 和 Dataloader (基于 GraphQL 构建)这样的库已经开发出来帮助处理类似的场景。 然而,它仍然没有完全覆盖像浏览器和移动缓存这样的东西。

查询性能

Graphql 为客户机提供了执行查询的能力,以获得他们所需要的东西。 这是一个惊人的功能,但是,它可能是一个有点争议的,因为它也可能意味着用户可以要求许多领域的许多资源,因为他们想要的。

例如,用户定义了一个查询,要求提供对特定艺术家的所有曲目进行评论的所有用户的列表。 这需要这样的查询:

artist(id: '1') {
  id
  name
  tracks {
    id
    title
    comments {
      text
      date
      user {
        id
        name
      }
    }
  }
}

这个查询可能会得到成千上万的数据作为响应。

因此,尽管允许用户请求他们需要的任何东西是一件好事,但是在一定的复杂度水平上,这样的请求会降低性能并极大地影响 GraphQL 应用程序的效率。

对于复杂的查询,REST API 可能更容易设计,因为您可以为特定需求设置多个端点,并且对于每个端点,您可以定义特定的查询以高效的方式检索数据。 这也可能是一个有点争议的事实,几个网络调用也可能需要很多时间,但如果你不小心,一些大的查询可以使你的服务器瘫痪。

数据不匹配

正如我们之前所示,在后端使用 GraphQL 构建时,数据库和 GraphQL API 通常具有相似但不同的模式,这些模式转换为不同的文档结构。 因此,来自数据库的跟踪将具有 trackId 属性,而通过 API 获取的相同跟踪将在客户机上具有跟踪属性。 这导致客户机 / 服务器端数据不匹配。

考虑在客户端获取特定曲目的艺术家名称,它看起来像这样:

const getArtistNameInClient = track => {
  return artist.user.name
}

然而,在服务器端执行完全相同的操作将导致完全不同的代码,如下所示:

const getArtistNameInServer = track => {
  const trackArtist = Users.findOne(track.userId)
  return trackArtist.name
}

通过扩展,这意味着您错过了 GraphQL 在服务器上查询数据的伟大方法。 值得庆幸的是,这并非没有解决办法。 事实证明,您可以很好地运行服务器到服务器的 GraphQL 查询。 怎么做到的? 你可以通过将你的 GraphQL 可执行模式传递给 GraphQL 函数,以及你的 GraphQL 查询来实现:

const result = await graphql(executableSchema, query, {}, context, variables);

根据 Sesha Greif 的说法,重要的是不要仅仅将 GraphQL 看作纯客户机-服务器协议。 Graphql 可以用于在任何情况下查询数据,包括使用 Apollo Link State 进行客户对客户的查询,甚至在使用 Gatsby 进行静态构建过程时也可以使用。

图式相似性

在后端使用 GraphQL 构建时,似乎无法避免重复和代码重复,尤其是涉及模式时。 首先,您需要为数据库和 GraphQL 端点分别提供一个模式和一个模式,这涉及到类似但不完全相同的代码,特别是在模式方面。

定期为模式编写非常类似的代码已经够难的了,但是更令人沮丧的是,您还必须不断地保持它们同步。

显然,其他开发人员已经注意到了这个问题,到目前为止,GraphQL 社区已经在努力解决这个问题。 以下是我们发现的两种最常用的修复方法:

  1. Postgraphile 从您的 PostgreSQL 数据库生成一个 GraphQL 模式,并且
  2. 还可以帮助您生成查询和突变的类型。

总结

Graphql 是一项令人兴奋的新技术,但是在做出昂贵而重要的架构决策之前,理解其中的权衡是很重要的。 一些 api,比如那些只有很少的实体和跨实体的关系的 api,比如分析 api,可能不太适合 GraphQL。 然而,具有许多不同领域对象的应用程序(如电子商务应用程序,其中包含项目、用户、订单、付款等等)可能能够更好地利用 GraphQL。

是一个强大的工具,在你的项目中有很多理由选择它,但是千万不要忘记,最重要的也是最好的选择,就是在考虑中选择适合项目的工具。 我在这里介绍的优点和缺点可能并不总是适用,但是在查看 GraphQL 时值得考虑它们,看看它们是否能够帮助您的项目,或者了解缺点是否已经得到解决。