发布于 2026-01-05 4 阅读
0

GraphQL实战:简介

GraphQL实战:简介

什么是GraphQL?它的设计理念是什么?它与其他替代方案有何不同?它的优点和缺点是什么?


我正在为 Manning 出版社撰写《GraphQL In Action》一书,其中 6 章(共 11 章)已在 MEAP 上发布。

以下是本书第一章全文。欢迎并非常感谢您的反馈。


本章内容

  • GraphQL是什么以及它背后的设计理念
  • GraphQL 与 REST API 等其他替代方案有何不同?
  • GraphQL系统的语言和服务部分
  • GraphQL 的优点和缺点

需求是发明之母。GraphQL 的灵感源于 Facebook 开发的一个产品,因为他们需要解决移动应用中的诸多技术难题。然而,我认为 GraphQL 之所以能如此迅速地流行起来,并非因为它解决了技术问题,而是因为它解决了通信问题。

沟通并非易事。提升沟通技巧能从多方面改善我们的生活,同样地,改善软件应用程序各部分之间的沟通,也能使该应用程序更容易理解、开发、维护和扩展。

这就是我认为 GraphQL 具有颠覆性意义的原因。它彻底改变了软件应用不同“端”(前端和后端)之间的通信方式。它赋予它们平等的权力,使它们彼此独立,将它们的通信过程与其底层技术传输通道解耦,并在以往通用语言仅限于寥寥数语的领域引入了一种全新的、丰富的语言。

如今,GraphQL 为 Facebook 的众多应用提供支持,包括 facebook.com 的主网页应用、Facebook 移动应用以及 Instagram。开发者对 GraphQL 的兴趣显而易见,其应用也正在迅速增长。除了 Facebook,GraphQL 还被许多其他主流的 Web 和移动应用所采用,例如 GitHub、Yelp、Pinterest、Twitter、《纽约时报》、Coursera 和 Shopify。考虑到 GraphQL 是一项新兴技术,这样的应用列表令人印象深刻。

在本章中,我们将学习 GraphQL 究竟是什么,它解决了哪些问题,又带来了哪些问题!

什么是 GraphQL

GraphQL 中的“图”一词源于这样一个事实:在现实世界中,表示数据的最佳方式就是使用图数据结构。无论数据模型大小,分析任何数据模型,你都会发现它始终是一个对象图,对象之间存在着许多关系。

那是我开始学习 GraphQL 时的第一个“顿悟”时刻。为什么要把数据看作是资源(URL)的形式,甚至是连接表,而不是把它看作一个图呢?

GraphQL 中的“QL”可能有点令人困惑。没错,GraphQL 是一种用于数据 API 的“查询语言”,但这仅仅是从前端用户的角度来看的。GraphQL 同时也是一个运行时层,需要在后端实现,正是这个运行时层使得前端用户能够使用这种新的“语言”。

GraphQL“语言”的设计理念是声明式和高效的。数据API使用者(例如移动和Web应用程序)的开发者可以使用这种语言,以更贴近他们思维方式的语言来请求他们确切的数据需求,而不是使用与数据存储方式或数据关系实现方式相关的语言。

在后端,GraphQL 需要一个运行时环境。该运行时环境为服务器提供了一种结构,用于描述其 API 中要公开的数据。在 GraphQL 领域,我们称这种结构为“模式”(schema)。

任何客户端都可以使用 GraphQL 语言,根据后端模式构建一个能够准确表达其数据需求的文本。然后,客户端通过传输通道(例如 HTTP)将该文本发送到 API 服务。GraphQL 运行时层接收该文本请求,并与后端堆栈中的其他服务通信,以生成与该文本请求相匹配的数据响应。最后,它会将数据以 JSON 等格式发送回客户端。

GraphQL 是一种语言,也是一个运行时环境。

注意:GraphQL 不依赖于任何特定的后端或前端框架、技术栈或数据库。它可以用于任何前端环境、任何后端平台以及任何数据库引擎。您可以将其用于任何传输通道,并使其使用任何数据表示格式。

在前端 Web 或移动应用中,您可以使用 GraphQL,既可以借助 Apollo 或 Relay 等客户端,也可以手动向 GraphQL 服务器发送 Ajax 请求。您可以使用 React(或 React Native)等库来管理视图如何使用来自 GraphQL 服务的数据,也可以使用 UI 环境自带的 API(例如 DOM API 或原生 iOS 组件)来实现。

虽然在应用程序中使用 GraphQL 不需要 React、Apollo 或 Relay,但这些库可以增加利用 GraphQL API 的价值,而无需执行复杂的数据管理任务。

大局

通常来说,API(应用程序接口)是一种接口,它实现了应用程序中多个组件之间的通信。例如,API 可以实现 Web 客户端和数据库服务器之间的通信。客户端需要告诉服务器它需要哪些数据,而服务器则需要提供代表客户端请求数据的对象来满足客户端的需求。

数据API概览

API 有多种类型,每个大型应用程序都需要它们。当我们谈到 GraphQL 时,我们特指用于读取和修改数据的 API 类型,通常称为“数据 API”。

GraphQL 是众多可用于为应用程序提供可编程接口的选项之一,这些接口允许应用程序从数据服务中读取和修改所需数据。其他选项包括 REST、SOAP、XML,甚至 SQL 本身。

SQL(标准查询语言)可以直接与 GraphQL 进行比较,毕竟两者的名称中都包含“QL”。SQL 和 GraphQL 都提供了一种用于查询数据模式的语言,并且都可以用于读取和修改数据。

例如,假设我们有一个包含公司员工数据的表,以下是一个读取某个部门员工数据的 SQL 语句示例:

SELECT id, first_name, last_name, email, birth_date, hire_date
FROM employees
WHERE department = 'ENGINEERING'
用于查询的 SQL 语句

以下是另一个可用于插入新员工数据的 SQL 语句示例:

INSERT INTO employees (first_name, last_name, email, birth_date, hire_date)
VALUES ('John', 'Doe', 'john@doe.name', '01/01/1990', '01/01/2020')
用于修改的 SQL 语句

你可以使用 SQL 来执行数据操作,就像我们上面做的那样。这些 SQL 语句发送到的关系数据库通常支持不同的响应格式。每种 SQL 操作类型都会有不同的响应。SELECT 操作可能返回单行或多行数据。INSERT 操作可能只返回确认信息、已插入的行或错误响应。

提示:虽然可行,但 SQL 并非移动和 Web 应用直接传递数据需求的理想语言。SQL 功能过于强大且灵活,会带来诸多挑战。例如,公开数据库的具体结构会带来严重的安全隐患。您可以将 SQL 置于另一个服务层之后,但这需要您编写解析器和分析器,在将用户的 SQL 查询发送到数据库之前对其进行处理。而任何 GraphQL 服务器实现都已内置了这种解析器/分析器。

虽然大多数关系型数据库都直接支持 SQL,但 GraphQL 则完全不同。GraphQL 需要自己的运行时服务。你还不能直接使用 GraphQL 查询语言来查询数据库(至少目前还不行)。你需要使用支持 GraphQL 的服务层,或者自己实现一个。

JSON 是一种可以用来传输数据的语言。以下是一个可以表示 John 数据的 JSON 文本示例:

{
  "data": {
    "employee":{
      id: 42,
      name: "John Doe",
      email: "john@doe.name",
      birthDate: "01/01/1990",
      hireDate: "01/01/2020"
    }
  }
}
表示数据的 JSON 对象

注意:关于约翰的数据不必完全按照数据库中保存的结构来呈现。我使用了驼峰式命名法,并将 first_name 和 last_name 合并为一个 name 字段。

JSON 是一种流行的语言,用于在 API 服务器和客户端应用程序之间传递数据。大多数现代数据 API 服务器都使用 JSON 来满足客户端应用程序的数据需求。GraphQL 服务器也不例外;JSON 是满足 GraphQL 数据请求需求的常用选择。

客户端应用程序也可以使用 JSON 向 API 服务器传达其数据需求。例如,以下是一个可用于传达员工对象响应数据需求的 JSON 对象示例:

{
  "select": {
    "fields": ["name", "email", "birthDate", "hireDate"],
    "from": "employees",
    "where": {
      "id": {
       "equals": 42
      }
    }
  }
}
JSON 查询示例

GraphQL 是客户端应用程序可以用来表达数据需求的另一种语言。以下是如何用 GraphQL 查询表达相同的数据需求:

{
  employee(id: 42) {
    name
    email
    birthDate
    hireDate
  }
}
GraphQL 查询示例

上面的 GraphQL 查询与 JSON 对象表达了相同的数据需求,但正如您所见,它的语法更简洁。GraphQL 服务器可以理解这种语法,并将其转换为实际数据存储引擎能够理解的格式(例如,将其转换为关系数据库的 SQL 语句)。然后,GraphQL 服务器可以接收存储引擎的响应,并将其转换为 JSON 或 XML 等格式,然后发送回客户端应用程序。

这很好,因为无论你使用什么存储引擎(或多个存储引擎),使用 GraphQL,你都可以使 API 服务器和客户端应用程序都使用通用的请求语言和通用的响应语言。

简而言之,GraphQL 的核心在于优化客户端与服务器之间的数据通信。这包括客户端请求所需数据并将其传递给服务器,服务器准备满足该需求的响应并将其返回给客户端。GraphQL 允许客户端请求所需的确切数据,并使服务器能够更轻松地从多个数据存储资源聚合数据。

GraphQL 的核心在于其强大的类型系统,该系统用于描述数据并组织 API。这种类型系统为 GraphQL 在服务器端和客户端都带来了诸多优势。类型确保客户端只请求可行数据,并提供清晰易懂的错误信息。客户端可以利用类型来最大程度地减少手动解析数据元素的工作。GraphQL 类型系统支持丰富的特性,例如自省式 API 以及为客户端和服务器构建强大的工具。GraphiQL 就是一款基于此概念的流行 GraphQL 工具,它是一个功能丰富的基于浏览器的编辑器,用于探索和测试 GraphQL 请求。您将在下一章学习 GraphiQL。

GraphQL 是一种规范

虽然 Facebook 的工程师早在 2012 年就开始研发 GraphQL,但直到 2015 年才发布了公开的规范文档。您可以通过访问jscomplete.com/graphql-spec查看该文档的最新版本。

本文档由 GitHub 上的公司和个人社区维护。GraphQL 仍然是一门不断发展的语言,但其规范文档为该项目奠定了良好的开端,因为它定义了所有 GraphQL 运行时实现者都必须遵守的标准规则和实践。目前已有许多使用不同编程语言实现的 GraphQL 库,它们都严格遵循规范文档,并在文档更新时更新自身的实现。如果您使用 Ruby 开发一个 GraphQL 项目,之后切换到另一个 Scala 项目,语法会发生变化,但规则和实践仍然保持不变。

最终,你可以在官方规范文档中了解关于 GraphQL 语言和运行时要求的全部内容。虽然它有点技术性,但你仍然可以通过阅读其引言部分和示例学到很多东西。本书不会涵盖文档中的所有内容,所以我建议你在读完本书后浏览一下该文档。

规范文档首先描述了GraphQL语言的语法。我们先来谈谈语法。

:除了规范文档之外,Facebook 还发布了一个用于 GraphQL 运行时的 JavaScript 参考实现库。JavaScript 是最流行的编程语言,也是最接近移动和 Web 应用的语言,而移动和 Web 应用正是 GraphQL 能够发挥巨大作用的两大热门应用领域。GraphQL 的 JavaScript 参考实现托管在github.com/graphql/graphql-js,本书将使用这个实现。我将把这个实现称为“GraphQL.js”。

GraphQL 是一种语言

虽然名称中带有“Q”(代表查询),但查询通常与读取数据相关,而 GraphQL 既可以用于读取数据,也可以用于修改数据。使用 GraphQL 读取数据时,使用查询(query);修改数据时,使用变更(mutation)。查询和变更都是 GraphQL 语言的一部分。

注意:除了查询变更之外,Graphql 还支持第三种请求类型,称为订阅,它用于实时数据监控请求。

这就像你使用 SQL 的 SELECT 语句读取数据,使用 INSERT、UPDATE 和 DELETE 语句修改数据一样。SQL 语言有一些必须遵守的规则。例如,SELECT 语句必须包含 FROM 子句,并且可以选择性地包含 WHERE 子句。同样,GraphQL 语言也有一些必须遵守的规则。例如,GraphQL 查询必须有一个名称,或者必须是请求中唯一的查询。你将在接下来的几章中学习 GraphQL 语言的规则。

像 GraphQL(或 SQL)这样的查询语言与 JavaScript 或 Python 等编程语言截然不同。你不能直接使用 GraphQL 语言来创建用户界面或执行复杂的计算。查询语言的应用场景更为具体,而且通常需要与其他编程语言配合使用才能正常工作。不过,我希望你首先将查询语言的概念与编程语言,甚至是我们日常使用的语言(例如英语)进行比较。虽然这种比较范围很窄,但我认为对于 GraphQL 来说,它能帮助你理解并欣赏它的一些特性。

编程语言的演进总体上正使其越来越接近我们日常使用的语言。过去,计算机只能理解命令式指令,因此我们一直使用命令式编程范式来编写程序。然而,如今计算机开始理解声明式编程范式,我们可以编写程序让它们理解我们的愿望。声明式编程有很多优点(和缺点),但它之所以如此出色,是因为我们总是倾向于用声明式的方式来思考问题。声明式思维对我们来说更容易理解和运用。

我们可以使用英语来清晰地表达数据需求和实现方式。例如,假设 John 是客户端,Jane 是服务器。以下是一个英语数据沟通示例:

约翰:嘿,简,阳光到达地球需要多长时间?

简:大概8分钟多一点。

约翰:那月光呢?

简:不到2秒。

约翰可以很轻松地用一句话提出这两个问题,而简也可以通过在答案中添加更多词语来轻松地回答这两个问题。

当我们用英语交流时,我们能理解“a bit over”和“a bit under”这样的特殊表达。简也明白,第二个未完成的问题与第一个问题相关。另一方面,计算机目前还不太擅长从上下文中理解事物。它们需要更清晰的结构。

GraphQL 只是约翰和简可以用来进行数据通信的另一种声明式语言。它不如英语流畅易懂,但它是一种结构化的语言,计算机可以轻松解析和使用。例如,以下是一个假设的 GraphQL 查询,可以表示约翰向简提出的两个问题:

{
  timeLightNeedsToTravel(toPlanet: "Earth") {
    fromTheSun: from(star: "Sun")
    fromTheMoon: from(moon: "Moon")
  }
}
John 在 GraphQL 中向 Jane 提出的问题

这个 GraphQL 请求示例使用了 GraphQL 语言的一些组成部分,例如字段(`fields`timeLightNeedsToTravelfrom`datafields`)、参数(`parameters`、`parameters`toPlanetstar` moonparameters`)以及别名(` aliases`fromTheSun和 ` fromTheMoonaliases`)。它们类似于英语中的动词和名词。您将在第 2 章和第 3 章中学习所有可以在 GraphQL 请求中使用的语法部分。

GraphQL 是一种服务

如果我们教会客户端应用程序使用 GraphQL 语言,它就能将任何数据需求传递给同样支持 GraphQL 的后端数据服务。要让数据服务支持 GraphQL,你需要实现一个运行时层,并将该层暴露给想要与该服务通信的客户端。你可以把服务器端的这个运行时层想象成 GraphQL 语言的翻译器,或者说是一个代表数据服务的 GraphQL 代理。GraphQL 本身并不是存储引擎,因此它无法独立构成完整的解决方案。这就是为什么你不能只使用 GraphQL 语言的服务器,而需要实现一个翻译运行时层的原因。

GraphQL 服务可以用任何编程语言编写,从概念上讲,它可以分为两个主要部分:结构和行为。

  1. GraphQL 的结构由强类型模式定义。GraphQL 模式就像一个目录,记录了 GraphQL API 可以处理的所有操作。它简单地表示了 API 的功能。GraphQL 客户端应用程序使用模式来了解它们可以向服务提出哪些问题。模式的类型化是 GraphQL 的核心概念。模式本质上是一个字段图,每个字段都有自己的类型,该图表示所有可以通过服务读取(或更新)的数据对象。

  2. 这种行为自然是通过函数实现的,在 GraphQL 世界中,这些函数被称为解析器函数,它们代表了 GraphQL 强大功能和灵活性背后的大部分智能逻辑。GraphQL schema 中的每个字段都由一个解析器函数支持。解析器函数定义了要为其字段获取哪些数据。

解析器函数用于向运行时服务提供指令,告诉它如何以及从何处访问原始数据。例如,解析器函数可以向关系数据库发出 SQL 语句、直接从操作系统读取文件数据,或者更新文档数据库中的缓存数据。解析器函数与 GraphQL 请求中的字段直接相关,它可以表示单个原始值、对象或值/对象列表。

GraphQL餐厅类比

GraphQL schema 经常被比作餐厅菜单。在这个比喻中,服务员就像是 GraphQL API 接口的实例。难怪他们会用“服务员”这个词!

服务员会将您的订单带回厨房,厨房是 API 服务的核心。您可以将菜单上的菜品与 GraphQL 语言中的“字段”进行比较。如果您点了一份牛排,您需要告诉服务员您想要几分熟。这时您就可以使用字段参数了!

订单 {
牛排(熟度:七分熟)
}

假设这家餐厅生意非常火爆。他们聘请了一位厨师,专门负责烹制牛排。这位厨师就是牛排领域的最终解决者!

提示:解析器函数是 GraphQL 常被拿来与远程过程调用 (RPC) 分布式计算概念相比较的原因。GraphQL 本质上是客户端调用远程解析器函数的一种方式。

模式和解析器示例

为了理解解析器的工作原理,我们来看一个简化的employee查询,并假设客户端将其发送到 GraphQL 服务:

query {
  employee(id: 42) {
    name
    email
  }
}

简化示例查询文本

该服务可以接收并解析任何请求。然后,它会尝试根据其模式验证请求。该模式必须支持一个顶级employee字段,并且该字段必须表示一个包含一个id参数、一个name字段和一个email字段的对象。字段和参数在 GraphQL 中必须具有类型。id参数可以是整数。参数nameemail字段可以是字符串。employee字段是自定义类型(表示特定的 id/name/email 结构)。

与客户端查询语言类似,GraphQL 社区也标准化了一种专门用于创建 GraphQL schema 对象的服务器端语言。这种语言被称为“模式语言”(Schema Language),通常缩写为 SDL(模式定义语言)或 IDL(接口定义语言)。

以下是使用 GraphQL 的模式语言表示“员工”类型的示例:

type Employee(id: Int!) {
  name: String!
  email: String!
}
GraphQL模式语言示例

这是Employee表示员工“模型”结构的自定义类型。可以使用整数查找员工模型对象id,它包含name字符串email字段。

注意:类型后的感叹号表示该类型不能为空。客户端请求员工字段时必须指定 id 参数,并且服务器对此字段的有效响应必须包含姓名字符串和电子邮件字符串。

提示:模式语言类型定义类似于我们用来定义表(和其他数据库模式元素)的 CREATE 语句。

利用这种类型,GraphQL 服务可以判断employeeGraphQL 查询有效,因为它与支持的类型结构匹配。下一步是准备它请求的数据。为此,GraphQL 服务会遍历请求中的字段树,并调用与每个字段关联的解析器函数。然后,它会收集这些解析器函数返回的数据,并用这些数据生成一个响应。

这个 GraphQL 服务示例至少需要 3 个解析器函数:一个用于字段employee,一个用于name字段,一个用于email字段。

例如,员工字段的解析函数可能会执行如下查询: 。这SQL语句会返回员工select * from employees where id = 42中的所有列。假设员工表恰好包含以下字段id,,,,,first_namelast_nameemailbirth_datehire_date

因此,员工编号为 42 的员工字段解析函数可能会返回类似这样的对象:

{
  id: 42,
  first_name: 'John',
  last_name: 'Doe',
  email: 'john@doe.com'
  birth_date: "01/01/1990",
  hire_date: "01/01/2020"  
}
数据库中关于员工 #42 的回复

GraphQL 服务会继续逐个遍历树中的字段,并为每个字段调用解析器函数。每个解析器函数都会接收其父节点解析器函数的执行结果。因此,`get`name和 ` emailget` 解析器函数都会接收这个对象(作为它们的第一个参数)。

假设我们有以下(JavaScript)函数,分别代表服务器端解析器中 ` nameand`email字段的解析函数:

// Resolver functions
const name => (source) => `${source.first_name} ${source.last_name}`;
const email => (source) => source.email;

这里的对象source是父节点。对于顶级字段,该source对象通常是未定义的(因为没有父节点)。

提示:该email解析器函数被称为“简单”解析器,因为“email”字段名与父源对象上的“email”属性名匹配。某些GraphQL实现(例如JavaScript实现)内置了这些简单解析器,并在找不到字段解析器时将其用作默认解析器。

GraphQL 服务将使用这 3 个解析器函数的所有响应,为 GraphQL 查询生成以下单个响应employee

{
  data: {
    employee: {
      name: 'John Doe',
      email: 'john@doe.com'
    }
  }
}
示例 GraphQL 响应对象

我们将在第 5 章开始探讨如何编写自定义解析器。

提示:GraphQL 对数据序列化格式没有特定要求,但 JSON 是最常用的格式。本书中的所有示例都将使用 JSON 格式。

为什么选择 GraphQL

GraphQL并非唯一(甚至不是第一个)旨在促进高效数据API创建的技术。您可以使用基于JSON的API并搭配自定义查询语言,或者在REST API之上实现开放数据协议(OData)。经验丰富的后端开发人员早在GraphQL出现之前就已经开始创建高效的数据API技术了。那么,我们究竟为什么需要一项新技术呢?

如果让我用一个词来回答“为什么选择 GraphQL”这个问题,那这个词就是:标准

GraphQL 提供了可维护和可扩展的 API 功能实现标准和结构,而其他替代方案则缺乏此类标准。

GraphQL 强制要求数据 API 服务器发布关于其功能的“文档”(即模式)。该模式使客户端应用程序能够了解这些服务器上可供它们使用的所有资源。GraphQL 标准模式必须是每个 GraphQL API 的一部分。客户端可以使用 GraphQL 语言向服务查询其模式。我们将在第 3 章中看到相关示例。

其他解决方案也可以通过添加类似的文档来改进。GraphQL 的独特之处在于,文档是 API 服务创建过程的一部分。您不能使用过时的文档,也不能忘记记录用例,更不能提供不同的 API 使用方式,因为您必须遵循标准。最重要的是,您无需将 API 文档与 API 本身分开维护。GraphQL 文档是内置的,而且是一流的!

GraphQL 的强制性模式定义了 GraphQL 服务能够回答的问题类型及其局限性,但由于 GraphQL 本质上是一个节点图,而图可以通过多种路径遍历,因此模式的使用方式具有一定的灵活性。这种灵活性是 GraphQL 的一大优势,因为它允许后端和前端开发人员在项目开发过程中无需频繁协调即可取得进展。GraphQL 从根本上实现了客户端与服务器的解耦,使二者能够独立演进和扩展。这极大地加快了前端和后端产品的迭代速度。

我认为这种标准模式是 GraphQL 的主要优势之一,但我们也来谈谈 GraphQL 的技术优势吧。

在客户端和服务器之间引入 GraphQL 层的最大技术原因之一,或许也是最普遍的原因,就是效率。API 客户端通常需要向服务器查询多个资源,而 API 服务器通常只知道如何回答关于单个资源的问题。因此,客户端最终需要多次与服务器通信才能获取所需的所有数据。

客户端向服务器查询多个资源

借助 GraphQL,您可以将这种多请求的复杂性转移到后端,并让 GraphQL 运行时来处理。客户端只需向 GraphQL 服务提出一个问题,即可获得包含客户端所需信息的单一响应。虽然您可以自定义基于 REST 的 API,为每个视图提供一个特定的端点,但这并非标准做法。您需要自行实现,而没有标准指南可循。

GraphQL 将多请求的复杂性转移到后端。

GraphQL 的另一项重大技术优势在于能够与多个服务进行通信。当多个客户端需要从多个数据存储服务(例如 PostgreSQL、MongoDB 和 Redis 缓存)请求数据时,中间的 GraphQL 层可以简化并标准化这种通信。客户端无需直接访问多个数据服务,而是可以先与 GraphQL 服务通信。然后,GraphQL 服务会负责与不同的数据服务进行通信。这就是 GraphQL 如何避免客户端使用多种语言进行通信的原因。GraphQL 服务会将单个客户端的请求转换为使用不同语言向多个服务发送的多个请求。

GraphQL 可以与不同的数据服务进行通信

GraphQL 是一个翻译器

想象一下,有三个人,他们说着三种不同的语言,拥有不同类型的知识。现在,你有一个问题,只有结合这三个人的知识才能解答。如果你有一位精通这三种语言的翻译,那么找到问题的答案就变得轻而易举了。GraphQL 服务可以轻松地为客户端实现这一点。虽然其他数据 API 也支持这种功能,但 GraphQL 提供了标准化的结构,能够以更简单、更易于维护的方式满足这类数据需求。

GraphQL 的另一个常被低估的优势在于它能显著提升前端开发者的体验。GraphQL schema 赋予前端开发者强大的能力和控制权,让他们能够自主地探索、构建、验证、测试并准确地执行数据通信,而无需依赖后端开发者。它消除了服务器对数据结构或大小进行硬编码的需要,并将客户端与服务器解耦。这意味着客户端和服务器可以独立开发和维护,这本身就是一个巨大的优势。

更重要的是,借助 GraphQL,开发者可以使用声明式语言来表达用户界面的数据需求。他们表达的是“需要什么”,而不是“如何提供”。用户界面所需的数据与开发者在 GraphQL 中描述这些数据需求的方式之间存在着紧密的联系。

REST API 怎么样?

GraphQL API 经常被拿来与 REST API 比较,因为后者一直是 Web 和移动应用数据 API 的首选。GraphQL 提供了一种比 REST API 更高效的“技术”替代方案。但我们为什么需要替代方案呢?REST API 到底有什么问题?

REST API 最大的“相关”问题在于客户端需要与多个数据 API 端点通信。REST API 就是一个典型的服务器示例,它要求客户端进行多次网络往返才能获取数据。REST API 是一组端点的集合,每个端点代表一个资源。因此,当客户端需要获取多个资源的数据时,它需要向该 REST API 发送多个网络请求,然后通过组合收到的多个响应来获得所需的数据。这对于移动应用程序来说尤其成问题,因为移动设备通常存在处理能力、内存和网络限制。

此外,REST API 没有客户端请求语言。客户端无法控制服务器返回的数据,因为它们没有合适的语言来表达自己的确切需求。更准确地说,REST API 客户端可用的语言非常有限。例如,READ REST API 端点只有以下几种:

  • GET /ResourceName- 获取该资源的所有记录列表,或
  • GET /ResourceName/ResourceID- 获取由 ID 标识的单个记录。

在纯 REST API(非定制 API)中,客户端无法指定要从资源中选择哪些字段。这些信息都存储在 REST API 服务本身,无论客户端实际需要哪些字段,REST API 服务始终会返回所有字段。GraphQL 将此类问题称为“过度获取不需要的信息”。这会浪费客户端和服务器的网络和内存资源。

REST API 的另一个主要问题是版本控制。如果需要支持多个版本,通常意味着需要创建新的端点。这会导致在使用和维护这些端点时出现更多问题,并且可能造成服务器端代码重复。

注意:这里提到的 REST API 问题是 GraphQL 试图解决的特定问题,当然并非 REST API 的所有问题。

REST API 最终会演变成既包含常规 REST 端点,又包含为提升性能而定制的临时端点的混合体。而 GraphQL 则提供了一种更优的替代方案。

需要指出的是,REST API 相较于 GraphQL API 具有一些优势。例如,缓存 REST API 响应比缓存 GraphQL API 响应要容易得多,这一点您将在本章最后一节中看到。此外,针对 REST 端点优化代码也比针对通用单一端点优化代码要容易得多。当然,并不存在一种万能的解决方案可以解决所有问题而不引入新的挑战。REST API 有其存在的价值,如果使用得当,GraphQL 和 REST 都能发挥其强大的作用。而且,也没有任何规定禁止在同一个系统中同时使用这两种技术。

类 REST API

请注意,本书讨论的是纯 REST API。文中提到的一些问题,虽然可以通过 GraphQL 解决,但也可以通过定制 REST API 来解决。例如,您可以修改 REST API,使其接受一个“include”查询字符串,该字符串接受一个以逗号分隔的字段列表,用于指定要返回的响应字段。这也能避免过度获取数据的问题。您还可以让 REST API 通过一些查询标志来包含子资源。市面上有一些工具可以添加到您的 REST 系统中,它们可以启用此类定制功能,或者简化定制的实现过程。

这种方法在小规模项目中或许可行,我个人也曾成功使用过。然而,与 GraphQL 相比,这些方法需要大量工作,会导致项目迭代速度变慢。此外,它们缺乏标准化,难以扩展到大型项目。

GraphQL 之道

要了解 GraphQL 如何解决我们讨论过的 REST API 问题,您需要理解 GraphQL 背后的概念和设计决策。以下是几个主要方面:

1)类型化图模式

要创建 GraphQL API,你需要一个类型化的 schema。GraphQL schema 包含具有类型的字段。这些类型可以是基本类型,也可以是自定义类型。GraphQL schema 中的所有内容都需要一个类型。正是这种静态类型系统使得 GraphQL 服务具有可预测性和可发现性。

2)声明式语言

GraphQL 具有声明式的数据需求表达特性。它为客户端提供了一种声明式语言,方便他们表达数据需求。这种声明式特性使得 GraphQL 语言的思维模式与我们用英语思考数据需求的方式非常接近,也使得使用 GraphQL API 比其他方案更加便捷。

3) 单一端点和客户端语言

为了解决多次往返的问题,GraphQL 将响应服务器简化为一个单一的端点。简而言之,GraphQL 将自定义端点的概念发挥到了极致,使整个服务器成为一个能够响应所有数据请求的智能端点。

与这种单一智能端点概念相辅相成的另一个重要概念是与该端点协同工作所需的富客户端请求语言。如果没有客户端请求语言,单一端点就毫无用处。它需要一种语言来处理自定义请求并返回相应的数据。

采用客户端请求语言意味着客户端将拥有控制权。他们可以精确地请求所需内容,服务器也会准确地返回他们请求的内容。这解决了过度获取不需要的数据的问题。

此外,客户明确提出所需数据,有助于后端开发人员更有效地分析数据使用情况以及哪些数据部分需求量更高。这些数据非常有用。例如,它可以根据使用模式扩展和优化数据服务,还可以用于检测异常情况和客户版本变更。

4)简易版本控制

在版本控制方面,GraphQL 有其独特的见解。它完全可以避免版本控制。基本上,你可以直接添加新的字段和类型,而无需删除旧的,因为你使用的是图结构,可以通过添加更多节点来灵活扩展。你可以在图中保留旧 API 的路径,并引入新的 API。API 会不断扩展,而无需创建新的端点。客户端可以继续使用旧功能,也可以逐步更新代码以使用新功能。

通过使用单一的演进版本,GraphQL API 使客户端能够持续访问新功能,并鼓励编写更简洁、更易于维护的服务器代码。

这一点对移动客户端尤其重要,因为你无法控制它们使用的 API 版本。移动应用一旦安装,可能多年来都会继续使用同一个旧版本的 API。在 Web 端,控制 API 版本很容易,因为你只需推送新代码并强制所有用户使用即可。但对于移动应用来说,这要困难得多。

这种简单的版本控制方法存在一些挑战。永久保留旧节点会带来一些弊端。需要投入更多维护精力来确保旧节点仍然能够正常工作。此外,API 用户可能会对哪些字段是旧字段、哪些是新字段感到困惑。GraphQL 提供了一种弃用(并隐藏)旧节点的方法,这样模式的读取者就只能看到新节点。一旦某个字段被弃用,可维护性问题就变成了旧用户还会继续使用它多久。好处在于,作为维护者,您可以借助客户端查询语言自信地回答“某个字段是否仍在被使用?”和“某个字段的使用频率如何?”这两个问题。甚至可以实现对不再使用的已弃用字段的自动化移除。

REST API 和 GraphQL API 的实际应用

让我们来看一个 REST API 和 GraphQL API 的一对一对比示例。假设你正在构建一个用于展示《星球大战》电影和角色的应用程序。你首先要处理的 UI 是一个用于显示单个《星球大战》角色信息的视图。该视图应该显示角色的姓名、出生年份、星球名称以及他出演的所有电影的片名。例如,对于达斯·维达,除了他的名字之外,该视图还应该显示他的出生年份(公元前 41.9 年)、他的星球名称(塔图因)以及他出演的 4 部《星球大战》电影的片名(《新希望》、《帝国反击战》、《绝地归来》、《西斯的复仇》)。

这种视图听起来很简单,但实际上这里涉及三种不同的资源:人物、星球和电影。这些资源之间的关系也很简单。我们可以很容易地推测出所需的数据结构。一个人对象属于一个星球对象,并且它会拥有一个或多个电影对象。

此视图的 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" }
      ]
    }
  }
}
UI 组件的 JSON 数据示例对象

假设数据服务能够提供这种精确的结构,以下是使用 React.js 等前端组件库表示其视图的一种可能方法:

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

// The PersonProfile Component:
Name: {data.person.name}
Birth Year: {data.person.birthYear}
Planet: {data.person.planet.name}
Films: {data.person.films.map(film => film.title)}
React.js 中的 UI 视图示例

这是一个非常简单的例子。我们在《星球大战》项目中的经验帮助我们设计了所需数据的结构,并弄清楚了如何在用户界面中使用这些数据。

请注意此 UI 视图的一个重要特点。它与 JSON 数据对象的关系非常清晰。该 UI 视图使用了 JSON 数据对象中的所有“键”。请参见上方花括号内的值。

那么,如何向 REST API 服务请求这些数据呢?

您需要获取某个人的信息。假设您知道此人的 ID,REST API 应该会通过类似这样的端点公开该信息:

GET - /people/{id}

此请求将提供birthYear该人的姓名、背景及其他信息。REST API 还将提供该人所在星球的 ID 以及该人参演的所有电影的 ID 数组。

此请求的 JSON 响应可能如下所示:

{
  "name": "Darth Vader",
  "birthYear": "41.9BBY",
  "planetId": 1
  "filmIds": [1, 2, 3, 6],
  ... [other information that is not needed for this view]
}

然后,为了读出这颗行星的名字,你会问:

GET - /planets/1

要查看电影片名,你会问:

GET - /films/1
GET - /films/2
GET - /films/3
GET - /films/6

收到服务器返回的全部六个响应后,您可以将它们组合起来,以满足视图所需的数据。

除了为了满足一个简单的用户界面的简单数据需求而不得不进行 6 次网络往返之外,整个方法本身也至关重要。你给出了如何获取数据以及如何处理数据使其适用于视图的指令。例如,你必须处理行星和电影的 ID,尽管视图实际上并不需要它们。你不得不手动合并多个数据对象,而你实现的视图本身只需要一个数据对象。

您可以尝试自己通过 REST API 获取这些数据。星球大战数据有一个非常优秀的 REST API,托管在https://swapi.co,您可以在其中构建与之前相同的数据对象。数据元素的名称可能略有不同,但端点结构相同。您需要进行 6 次 API 调用。此外,您还需要获取视图不需要的额外信息。

当然,SWAPI 只是 REST API 的一个纯粹实现,用于处理此类数据。可能存在更好的自定义实现,能够更轻松地满足此视图的数据需求。例如,如果 API 服务器实现了嵌套资源,并且理解了人物和电影之间的关系,则可以使用类似以下的方式读取电影数据(以及人物数据):

GET - /people/{id}/films

然而,纯粹的 REST API 本身并不具备这种功能。你需要请求后端工程师为你的视图创建自定义端点。这就是扩展 REST API 的现实。你只能通过添加自定义端点来高效地满足不断增长的客户端需求。管理这些自定义端点并非易事。

例如,如果您自定义了 REST API 端点以返回某个角色的电影数据,这对于您当前正在实现的视图来说非常适用。但是,将来您可能需要实现角色个人资料信息的精简版或完整版。或许您只需要显示该角色的一部电影,或者除了片名之外,还需要显示每部电影的描述。每增加一个新需求,就意味着需要对端点进行进一步的自定义,甚至需要创建全新的端点来优化新视图所需的通信。这种方法显然存在局限性。

现在我们来看看 GraphQL 方法。

GraphQL 服务器只是一个智能端点。传输通道无关紧要。如果您通过 HTTP 进行操作,HTTP 方法当然也无关紧要。假设您有一个通过 HTTP 暴露的 GraphQL 端点,地址为/graphql

由于您希望通过一次网络往返获取所需数据,因此需要一种方法来表达服务器解析所需的所有数据。您可以使用 GraphQL 查询来实现这一点:

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

GraphQL 查询本质上只是一个字符串,但它必须包含所有你需要的数据。这就是声明式查询的优势所在。

让我们比较一下用英语和 GraphQL 如何表达这个简单视图的数据需求。

# In English:

The view needs:

a person's name,

birth year,

planet's name,

and the titles of all their films.

# In GraphQL:
{
  person(ID: ...) {
    name
    birthYear
    planet {
      name
    }
    films {
      title
    }
  }
}
GraphQL 与英语

你看,GraphQL表达式和英文表达式有多接近?简直一模一样。此外,还可以将GraphQL查询与我们最初使用的JSON数据对象进行比较。

# GraphQL Query (Question):

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

# Needed JSON (Answer):
{
  "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" }
      ]
     }
  }
}
GraphQL 与 JSON

GraphQL 查询的结构与 JSON 数据对象完全相同,只是去掉了所有的“值”部分。如果将其理解为问答关系,那么问题就是去掉答案部分的答案语句。

如果答案是:+
拥有 ID 4 的星球大战角色的名字是达斯·维达

这个问题的恰当表述是去掉答案部分的陈述:+
(什么是)ID 为 4 的《星球大战》角色的名字?

同样的道理也适用于 GraphQL 查询。取一个 JSON 数据对象,移除所有“答案”部分(即值),最终得到的就是一个适合表示关于该 JSON 数据对象的问题的 GraphQL 查询。

现在,请将 GraphQL 查询与使用该查询的 UI 视图进行比较。GraphQL 查询中的每个元素都会在 UI 视图中使用,而 UI 视图中使用的每个动态部分也会出现在 GraphQL 查询中。

这种显而易见的映射关系是 GraphQL 最强大的功能之一。UI 视图清楚地知道它需要哪些数据,并且从视图代码中提取这些需求非常容易。编写 GraphQL 查询实际上就是直接从 UI 视图中提取用作变量的内容。如果从多个嵌套的 UI 组件的角度来看,每个 UI 组件都可以请求它所需的部分数据,而应用程序的数据需求可以通过将这些部分数据需求组合起来构建。GraphQL 提供了一种通过名为“片段”(Fragments)的功能来定义部分数据需求的方法。您将在第 3 章中学习 GraphQL 片段的相关内容。

此外,如果你反转这个映射模型,你会发现另一个强大的概念。如果你有一个 GraphQL 查询,你就能准确地知道如何在 UI 中使用它的响应,因为查询和响应的“结构”完全相同。你无需检查响应就能知道如何使用它,也无需任何 API 文档。一切都是内置的。

《星球大战》数据有一个托管在graphql.org/swapi-graphql的 GraphQL API 。您可以使用那里提供的 GraphiQL 编辑器来测试 GraphQL 查询。我们将在下一章讨论 GraphiQL 编辑器,但您可以先尝试在那里构建示例数据 person 对象。本书后面会介绍一些细微的差别,但以下是您可以使用此 API 读取同一视图数据需求的官方查询(以达斯·维达为例):

{
  person(personID: 4) {
    name
    birthYear
    homeworld {
      name
    }
    filmConnection {
      films {
        title
      }
    }
  }
}
星球大战示例的 GraphQL 查询

只需将此查询粘贴到编辑器区域并点击运行按钮即可。此请求将返回与视图所用结构非常接近的响应,您以接近英语的方式表达了数据需求,并且您将在一次网络往返中获取所有这些数据。

GraphQL 会取代 REST 吗?

我第一次接触 GraphQL 的时候,发推文说“REST API 可以安息了!”。玩笑归玩笑,我并不真的认为 GraphQL 会“取代”REST API。但我确实认为,在 Web 和移动应用使用的 API 方面,会有更多人选择 GraphQL 而不是 REST。REST API 有其用武之地,但我认为它并不适合 Web 和移动应用。

我喜欢这样想:GraphQL 之于 REST,就像 JSON 之于 XML 一样。XML 目前仍然被广泛使用,但我所知的几乎所有基于 Web 的 API 都使用 JSON 格式。

GraphQL 相比 REST API 具有许多优势,但我们也来谈谈 GraphQL 带来的挑战。

GraphQL 问题

完美的解决方案只是童话故事。GraphQL 的灵活性也带来了一些显而易见的问题和隐患。

安全

GraphQL 的一个重要威胁是资源耗尽攻击(又称拒绝服务攻击)。攻击者可以通过过于复杂的查询耗尽 GraphQL 服务器的所有资源。查询深层嵌套关系(例如用户 -> 好友 -> 好友 -> 好友……)或使用字段别名多次请求同一字段都非常简单。资源耗尽攻击并非 GraphQL 独有,但在使用 GraphQL 时,必须格外小心。

注意:这种资源耗尽问题也可能来自一些非恶意客户端应用程序,例如存在某些错误或实现不佳的应用程序。请记住,GraphQL 客户端可以自由请求所需的任何数据,因此它可能会一次性请求过多的数据。

您可以采取一些缓解措施。您可以预先对查询进行成本分析,并对用户可消耗的数据量进行限制。您还可以设置超时机制,终止耗时过长的请求。此外,由于 GraphQL 服务只是应用程序堆栈中的一层,因此您可以在 GraphQL 底层更有效地处理速率限制。

如果您要保护的 GraphQL API 端点并非公开,而是供您自己的客户端应用程序(Web 或移动端)内部使用,则可以使用白名单机制,预先批准服务器可以执行的查询。客户端只需使用查询唯一标识符 (URI) 请求服务器执行预先批准的查询即可。虽然这种方法会在服务器和客户端之间引入一些依赖关系,但可以使用一些自动化策略来缓解这个问题。例如,您可以允许前端工程师在开发过程中修改他们需要使用的查询和变更,然后在部署到生产服务器时自动将其替换为各自的唯一 ID。一些客户端 GraphQL 框架已经在测试类似的概念。

在使用 GraphQL 时,身份验证和授权是需要考虑的其他问题。您是在 GraphQL 解析过程之前、之后还是期间处理它们?

要回答这个问题,你可以把 GraphQL 看作是构建在你后端数据获取逻辑之上的领域特定语言 (DSL)。它只是客户端和实际数据服务之间的一层。你可以把身份验证和授权看作是另一层。GraphQL 本身并不负责身份验证或授权逻辑的实际实现,它并非为此而设计的。但是,如果你想在 GraphQL 之后添加这些层,你可以使用 GraphQL 在客户端和执行逻辑之间传递访问令牌。这与 REST API 中身份验证和授权的实现方式非常相似。

缓存和优化

GraphQL 让客户端数据缓存这项任务变得更具挑战性。REST API 的响应由于其字典特性,更容易缓存。特定的 URL 对应特定的数据,因此可以直接使用 URL 本身作为缓存键。

使用 GraphQL,你可以采用类似的基本方法,将查询文本作为键来缓存其响应。但这种方法存在局限性,效率不高,并且可能导致数据一致性问题。多个 GraphQL 查询的结果很容易重叠,而这种基本的缓存方法无法处理这种重叠情况。

这个问题有一个绝妙的解决方案。图查询意味着图缓存。如果您将 GraphQL 查询响应规范化为扁平化的记录集合,并为每条记录分配一个全局唯一 ID,则可以缓存这些记录,而无需缓存完整的响应。

但这并非一个简单的过程。其中会存在记录引用其他记录的情况,你需要管理一个循环图。填充和读取缓存都需要查询遍历。你可能需要实现一个单独的层来处理这种缓存逻辑。不过,这种方法比基于响应的缓存方式效率要高得多。

在使用 GraphQL 时,另一个最“著名”的问题就是通常所说的 N+1 SQL 查询问题。GraphQL 查询字段被设计成独立的函数,而使用数据库中的数据解析这些字段可能会导致每个解析的字段都产生一个新的数据库请求。

对于简单的 REST API 端点逻辑,通过优化构建的 SQL 查询,可以轻松分析、检测和解决 N+1 问题。但对于 GraphQL 动态解析字段,情况就没那么简单了。 

幸运的是,Facebook正在率先提出一种可能同时解决缓存问题和数据加载优化问题的解决方案,它叫做DataLoader。

顾名思义,DataLoader 是一个实用工具,可用于从数据库读取数据并将其提供给 GraphQL 解析器函数。您可以使用 DataLoader 代替直接使用 SQL 查询从数据库读取数据,DataLoader 将作为代理来减少您发送到数据库的 SQL 查询次数。

DataLoader 可以优化 GraphQL 和数据库之间的请求。

DataLoader 结合了批处理和缓存技术来实现这一目标。如果同一个客户端请求需要查询数据库多个信息,DataLoader 可以将这些问题合并,并从数据库批量加载答案。DataLoader 还会缓存这些答案,以便后续查询相同资源时可以使用。

提示:还有其他 SQL 优化策略可以使用。例如,您可以通过分析 GraphQL 请求来构建最优的基于连接的 SQL 查询。如果您使用的是具有高效原生功能的关系型数据库,可以连接数据表并重用之前解析过的查询,那么在许多情况下,基于连接的策略实际上可能比基于 ID 的批处理更高效。但是,基于 ID 的批处理可能更容易实现。

学习曲线

相比其他技术,使用 GraphQL 的学习曲线更为陡峭。开发基于 GraphQL 的前端应用程序的开发者需要学习 GraphQL 语言的语法。而实现 GraphQL 后端服务的开发者则需要学习的内容远不止语言本身,他们还需要学习 GraphQL 实现的 API 语法,以及模式、解析器等众多 GraphQL 运行时特有的概念。

例如,REST API 就不太会遇到这个问题,因为它们没有客户端语言,也不需要任何标准实现。您可以自由地以任何方式实现 REST 端点,因为您无需解析、验证和执行特定的语言文本。

概括

  • 在现实世界中,表示数据的最佳方式是使用图数据结构。数据模型就是一个关联对象的图。GraphQL 正是基于这一特性而设计的。

  • GraphQL 系统包含两个主要组件:查询语言和运行时层。查询语言供数据 API 的使用者用来请求其所需的确切数据;运行时层位于后端,负责发布描述数据模型功能和要求的公共模式。运行时层通过单个端点接收传入请求,并以可预测的数据响应解析传入的数据请求。传入的请求是用 GraphQL 查询语言编写的字符串。

  • GraphQL 的核心在于优化客户端与服务器之间的数据通信。GraphQL 允许客户端以声明式的方式请求所需的确切数据,并使服务器能够以标准方式聚合来自多个数据存储资源的数据。

  • GraphQL 拥有官方规范文档,其中定义了所有 GraphQL 运行时实现者都需要遵守的标准规则和实践。

  • GraphQL 服务可以用任何编程语言编写,其概念上可以分为两大部分:一是使用强类型模式定义的结构,该模式表示 API 的功能;二是使用称为解析器的函数自然实现的行为。GraphQL 模式是一个字段图,每个字段都有自己的类型。该图表示所有可以通过 GraphQL 服务读取(或更新)的数据对象。GraphQL 模式中的每个字段都由一个解析器函数支持。

  • GraphQL 与之前的替代方案的区别在于,它提供了一套标准和结构,能够以可维护和可扩展的方式实现 API 功能。其他替代方案则缺乏这样的标准。GraphQL 还解决了许多技术难题,例如无需进行多次网络往返以及需要在客户端处理多个数据响应。

  • GraphQL 带来了诸多挑战,尤其是在安全性和优化方面。由于其灵活性,保护 GraphQL API 需要考虑更多漏洞。缓存灵活的 GraphQL API 也比缓存固定 API 端点(例如 REST API)要困难得多。此外,GraphQL 的学习曲线也比许多其他替代方案更为陡峭。


感谢阅读!本书可在bit.ly/graphql-in-action获取。

文章来源:https://dev.to/samerbuna/graphql-in-action-introduction-233a