[译]怎样使用GraphQL

原文地址:www.howtographql.com/

1.介绍

GraphQL是一个新的API标准,具有高效,强大,灵活的特性,旨在替代REST模式。GraphQL由Facebook公司开发并开源,目前由来自世界各地的公司和开发者共同维护。

API已经在软件基础设施中变得无处不在。总之, 一个API定义了客户端如何从服务器加载数据。

作为GraphQL的核心点,它支持声明式数据获取,客户端可以从接口中,精确指定需要的数据。不同于多个后端接口返回固定的数据结构,一个GraphQL服务仅仅暴露一个接口,并精确的响应客户端想要的数据。

GraphQL - 一种API查询语言

今天,大多数应用都需要从服务器获取存在数据库中的数据,API需要提供获取数据的接口,以满足应用的需要。

GraphQL往往被误认为是一个数据库技术。这是不正确的,GraphQL是一种API查询语言,而不是数据库的查询语言。所以说,它是数据库无关的,可以用在任何使用API的场景中。

一个更有效的REST模式替代者

REST模式是目前流行的从服务端获取数据的方式。 当REST模式提出时,客户端程序都相对简单,并且发展速度也远落后于目前。REST模式也能很好的适用于诸多应用。然而,过去几年里,API领域有了根本性的变化。特别是,以下三个因素,对API的设计方式发出了挑战:

1.日益增多的移动端使用,需要数据加载过程更有效率

在移动端使用量变多的场景下,低功耗的设备和较差的网络环境是Facebook开发GraphQL最初的原因。GraphQL通过最小化网络传输时所需要的数据量,改善了这种环境中的应用使用情况。

2.各种不同的前端框架和平台

面对各种各样的前端框架和不同终端里的应用,开发并维护一套通用的API,用来满足左右需求,变得越来越难。但是通过GraphQL,客户端可以精确获取所需数据。

3.快速开发&快速添加特性

持续部署已成为许多公司标准,快速迭代和频繁的产品更新也变的不可或缺。如果使用REST模式API,服务端往往需要修改对外暴露的数据接口,来满足客户端在需求和设计上的变动。这阻碍了快速开发和产品迭代。

历史,环境和采用

GraphQL并不是React开发者专用的

Facebook在2012年开始,在他们的原生移动应用中使用GraphQL。但有趣的是,GraphQL目前主要被采用在Web相关技术环境里,反而在移动应用中获得的影响力很小。

Facebook的第一次公开谈论GraphQL是在React.js Conf 2015,并在不久之后宣布了他们的开源计划。因为Facebook总是在结合React谈论GraphQL,所以对于非React开发人员来说,需要花费一段时间才能明白GraphQL绝不是一个限于使用React的技术。

youtube截图
Dan Schafer & Jing Chen在React.JS Conf 2015公开介绍GraphQL。

一个迅速发展的社区

事实上,GraphQL可以在所有客户端与API通信的地方使用。有趣的是,包括Netflix和Coursera的一些公司也曾试探过相似的方案,使API交互更有效率。Coursera设想了一种类似的技术,让客户端指定其数据要求,Netflix甚至已经开源了他们的Falcor解决方案。但在GraphQL开源后,Coursera取消了计划,并搭上了GraphQL的车。

今天,GraphQL被许多不同的公司(如GitHub,Twitter,Yelp和Shopify)用于生产。

[cdn.hicous.com/pic/bism5.p…]

尽管如此年轻的技术,GraphQL已经被广泛采用。点击了解还有谁使用GraphQL

列出几个专注于GraphQL的组织,有GraphQL SummitGraphQL EuropeGraphQL RadioGraphQL Weekly

2.GraphQL是更好的REST

在过去十年中,REST已经成为设计Web API的标准。它提供了一些伟大的想法,如无状态服务和结构化资源访问。然而,基于REST的API太死板,不能满足客户端快速变化的调用需求。

GraphQL的出现是为了应对更多的灵活性和效率的需要!它解决了与REST API打交道时,开发人员遇到的许多REST的缺点和低效性。

为了说明REST和GraphQL在从API获取数据时的主要区别,我们来考虑一个简单的示例场景:在博客应用中,应用需要展示指定用户的帖子标题。同一页面里还会显示该用户的最新3个关注者的名字。 REST和GraphQL是如何解决这个业务的呢?

REST vs GraphQL之数据获取

使用REST API时,通常可以调用多个接口来获取数据。针对上述场景,首先可以调用/user/<id>接口来获取用户数据。紧接着,调用/users/<id>/posts返回该用户的所有帖子。最后,第三个接口可以是/users/<id>/followers,返回该用户的关注者列表。

REST示意图
使用REST,必须向三个不同的接口发出三个请求以获取所需的数据。同时你也拿到了额外的数据,因为接口会返回你不需要的其他信息。

另一方面,在GraphQL中,你只需向拥有数据的GraphQL服务端发送一条查询语句。服务端就会响应并返回带有查询结果的JSON对象。

GraphQL示意图
使用GraphQL,客户端可以精确地指定查询中需要的数据。 请注意,服务器响应的结构会精确地匹配查询中定义的结构。

不再获取多余或不足的数据

REST最常见的缺点之一是不能精准的获取数据。这是因为客户端获取数据的唯一方法是调用接口,而接口总是返回固定数据结构。如果想设计出完美的API,刚好能为客户端提供精确的数据,是非常困难的。

“用图形的方式思考,而不是端点”。GraphQL共同发明人Lee Byron的Lessons From 4 Years of GraphQL

过多获取:下载了多余的数据

过多获取,意味着客户端拿到了超出真正需要额外的数据。想象一下,在一个页面中,只需要展示一组用户名称列表。但在REST API中,通常会调用/users接口,来获取包含用户数据在内的JSON数组。然而,这个返回结果中可能也包含了用户的其他信息,例如。用户的生日,地址等。而我们仅仅是想展示用户名。

获取不足和n+1问题

另外一个问题是获取的数据不足,然后多发一次请求的问题。获取不足通常是指接口不能返回所需信息。客户端将不得不再次发起请求来获取数据。可能的场景是,客户端需要先得到列表数据,然后为列表中的每个元素再次发起请求来获取元素相关数据。

例如,假设刚才的应用中,需要展示出每个用户的最后三个关注者。 API也提供了接口/users/<user-id>/followers。为了能够展示所需的信息,应用需要向/users接口发起请求,然后针对每个用户再去调用/users/<user-id>/followers接口。

前端的快速产品迭代

使用REST API的常见模式是,根据应用页面的需要来设计接口。这样做很便捷,只要简单地在客户端调用对应接口,就能获取页面中所有需要的数据。

但是这种模式最主要的缺点是,不允许在前端快速迭代。随着UI层面的每一个改动,接口数据是否匹配,这一点就暴露了很高的风险。因此,后端就需要进行调整,以解决新的数据需求。这会导致生产力下降,特别是降低了将用户建议快速整合进产品的能力。

如果使用GraphQL,这个问题迎刃而解。得益于GraphQL的灵活性,客户端的更改不需要服务端有任何额外的工作。客户端可以指定其确切的数据要求,因此前端的设计和数据方面的需求变化时,后端工程师不需要进行调整。

后端深度分析

GraphQL可以对请求的数据进行细粒度的分析。由于每个客户端都准确地指定了它感兴趣的数据,因此,我们可以深入了解关于数据的使用方式。有了这些信息,就可以用来优化API,和去除不需要的字段。

使用GraphQL,还可以对服务器处理请求时,进行低级别的性能监控。 GraphQL使用resolver函数的概念来收集客户端请求的数据。通过resolver提供的测量和性能数据,可以为解决系统中的瓶颈问题提供重要的见解。

Schema和类型系统的好处

GraphQL使用丰富的类型系统来定义API的功能。 API中暴露的所有类型都使用GraphQL模式定义语言(SDL)在Schema中记录下来。Schema是客户端和服务端之间的协定,以定义客户端如何访问数据。

一旦定义了Schema,在前端和后端工作的团队可以在不进行多余沟通的情况下开展工作,因为他们都知道经由网络传输的数据的确定结构。

前端工程师可以通过mock所需的数据结构轻松的进行测试。一旦服务端准备就绪,客户端可以快速切换到真正的服务,接着从真正的API加载数据。

3.核心概念

在本章中,你将了解GraphQL的一些基本语言结构。 内容包括,初次认识,定义类型,查询和mutation的语法。我们还为你准备了一个基于graphql-up的沙箱环境,你可以使用它来实验学到的内容。

视图定义语言(SDL)

GraphQL有自己的类型系统,用于定义API的Schema。编写Schema的语法称为SDL。

以下是使用SDL定义简单类型Person的示例:

type Person {
  name: String!
  age: Int!
}
复制代码

这个Person类型有两个字段,nameage,是String和Int类型的,紧跟着的 ! 表示该字段是必需的。

可以在类型之间相互使用。在博客应用的例子中,一个Person可以与一个Post相关联:

type Post {
  title: String!
  author: Person!
}
复制代码

同时,另一方面,Person类型中添加相应类型。

type Person {
  name: String!
  age: Int!
  posts: [Post!]!
}
复制代码

请注意,我们刚刚创建了PersonPost之间的一对多关系,因为Person上的posts字段实际上是一个帖子数组。

使用查询语句获取数据

使用REST API时,会从指定接口加载数据。每个接口都明确定义了返回的数据结构。这意味着客户端需要的数据,已经在URL中制定好了。

GraphQL中采用的方式截然不同。GraphQL的API通常只暴露一个接口,而不是返回固定数据结构的多个接口。 这是因为返回的数据结构不是一成不变的,而是灵活的,让客户端来决定真正需要的是什么数据。

这意味着客户端需要向服务端发送更多信息来告知所需求的数据 - 这个额外信息称为查询。

基本查询

我们来看看客户端发送给服务端的简单查询示例:

{
  allPersons {
    name
  }
}
复制代码

在这查询中,allPersons字段称为查询的根字段。根字段之后的所有内容称为查询的payload。此查询中,指定的唯一payload字段是name

此查询将返回当前存储在数据库中的所有的人员列表。返回的示例如下:

{
  "allPersons": [
    { "name": "Johnny" },
    { "name": "Sarah" },
    { "name": "Alice" }
  ]
}
复制代码

请注意,每个Person在响应结果中只有name一个属性,服务端并没有返回age属性。 这正是因为name是在查询中指定的唯一字段。

如果客户端也想要获取年龄属性,那么要做的,仅仅是调整一下查询语句,在查询的payload中加入新的字段:

{
  allPersons {
    name
    age
  }
}
复制代码

GraphQL的一个重要优势是,允许直接通过嵌套的方式查询信息。例如,如果想获取一个Person写的所有posts,可以简单地按照类型的结构来获取数据:

{
  allPersons {
    name
    age
    posts {
      title
    }
  }
}
复制代码

查询时带上参数

在GraphQL中,每个在视图中定义的字段都可以拥有参数(零或多个)。例如,allPersons字段可以包含last参数,用来返回特定数量的Persons。相应的查询如下:

{
  allPersons(last: 2) {
    name
  }
}
复制代码

通过Mutation操作数据

在从服务端获取到数据之后,主流的应用程序总是要更改存储在后端的数据。使用GraphQL,这些更改是使用Mutation来完成的。 Mutation一般有这三种:

  • 创建新数据
  • 更新现有数据
  • 删除现有数据

Mutation遵循与查询相同的语法结构,但是它们始终需要从Mutation关键字开始。 以下是我们如何创建一个新的Person的例子:

mutation {
  createPerson(name: "Bob", age: 36) {
    name
    age
  }
}
复制代码

请注意,与之前写的查询类似,mutation也有一个根字段,上面的例子中,这个字段是createPerson。 我们之前已经讲到过关于查询参数的内容。例子中,createPerson接受nameage两个参数。

像查询一样,我们还可以为mutation指定一个payload,我们可以在其中请求新建的Person对象的其他属性。 在我们的例子中,我们指定的是nameage,虽然我们的例子并不是很有意义,因为我们获取的是我们已知的数据。但我们已经清晰的了解到,当请求是mutation时,也能够指定查询信息,这体现了mutation的强大之处,允许在一次请求来回中,从服务器获取新数据!

上面的mutation的响应数据是这样的:

"createPerson": {
  "name": "Bob",
  "age": "36",
}
复制代码

我们总是能了解到,当新增数据时,GraphQL会在服务端生成一个唯一的ID类型。扩展一下我们之前定义的Person类型,我们可以添加一个id字段,如下所示:

type Person {
  id: ID!
  name: String!
  age: Int!
}
复制代码

现在,当创建一个新的Person时,可以直接在mutation的payload中查询id,这是事先在客户端不知道的信息:

mutation {
  createPerson(name: "Alice", age: 36) {
    id
  }
}
复制代码

使用Subscription进行实时更新

今天,许多应用的另一个重要需求是与服务端进行实时连接,以便实时响应重要事件。针对这种场景,GraphQL提供了订阅的概念。

当客户端订阅了事件,它将与服务器建立一个稳定连接并保持住这个链接。任何时刻,当有事件触发时,服务端都会把相关信息推送到客户端。不同于查询和mutation,订阅代表的是将数据推送出去的的模式,而不是经典的“请求 - 响应”的模式。

订阅使用的语法和查询和mutation相同。我们用来订阅Person类型上的事件,如下所示:

subscription {
  newPerson {
    name
    age
  }
}
复制代码

在客户端将此订阅发送到服务端之后,会建立起一个链接。之后,每当mutation被执,用来创建新的Person时,服务端就会把有关此人的信息发送给客户端:

{
  "newPerson": {
    "name": "Jane",
    "age": 23
  }
}
复制代码

定义视图(Schema)

现在,你已经对查询,mutation和订阅有一个基本的了解,让我们将这些放在一起,学习如何编写一个视图,让你自己执行目前为止所有的示例。

视图是使用GraphQL API时最重要的概念之一。它指定了API的功能,并定义了客户端如何请求数据。它通常被视为服务器和客户端之间的协议。

通常,视图只是GraphQL类型的集合。 但是,在为API编写视图时,有一些特殊的根类型:

type Query { ... }
type Mutation { ... }
type Subscription { ... }
复制代码

查询,mutation和订阅是客户端发送请求的入口点。要启用我们前面看到的allPersons查询,查询类型必须如下定义:

type Query {
  allPersons: [Person!]!
}
复制代码

allPersons称为API的根字段。再考虑到,我们将last参数添加到allPersons的示例,我们必须如下定义Query:

type Query {
  allPersons(last: Int): [Person!]!
}
复制代码

类似地,对于createPersonmutation,我们必须向Mutation类型添加一个根字段:

type Mutation {
  createPerson(name: String!, age: String!): Person!
}
复制代码

请注意,根字段有两个参数,Person的nameage

最后,对于订阅,我们必须添加newPerson根字段:

type Subscription {
  newPerson: Person!
}
复制代码

将所有整合在一起,这是你在本章中看到的所有查询和mutation的完整模式:

type Query {
  allPersons(last: Int): [Person!]!
}

type Mutation {
  createPerson(name: String!, age: String!): Person!
}

type Subscription {
  newPerson: Person!
}

type Person {
  name: String!
  age: Int!
  posts: [Post!]!
}

type Post {
  title: String!
  author: Person!
}
复制代码

4.大局(架构)

GraphQL仅仅作为规范发布。 这意味着GraphQL实际上,不会是一个详细描述GraphQL服务端行为的完整文档。

如果你想使用GraphQL,你必须自己去搭建GraphQL服务。你可以选择任何编程语言来实现(例如这些可参考的实现方式)或通过选用像Graphcool这样的服务,它提供了一个功能强大的GraphQL API。

用例

在本节中,我们将介绍使用GraphQL服务的3种不同类型的架构:

1.连接数据库的GraphQL服务

2.GraphQL服务作为轻量的一层,在第三方接口,或一些现有系统之上。通过一个GraphQL API集成起来。

3.混合以上两种方式,通过相同的GraphQL API,同时可以访问数据库和第三方接口或现有旧系统。

上述这三种架构,代表了GraphQL的主要用例,并展现出了使用时的灵活性。

1.连接数据库的GraphQL服务

这种架构是greenfield项目中最常见的。在设置中,您有一个(Web)服务器实现GraphQL规范。 当查询请求到GraphQL服务器时,服务器读取查询的payload并从数据库中读取所需的信息。 这被称为查询的解析。然后,它按照官方规范中的描述构造响应对象,并将其返回给客户端。

需要注意的是,GraphQL实际上是传输层无关的。这意味着它可以和任意的网络协议一起使用。 因此,GraphQL服务器可能是基于TCP,Websockets或者其他网络协议的。

GraphQL也不关心是什么数据库以及用于存储数据的格式。你可以使用像AWS Aurora这样的SQL数据库或者像MongoDB这样的NoSQL数据库。

图片描述

一个标准的连接到单个数据库的,GraphQL服务器的greenfield架构。

2.集成现有系统的GraphQL层

GraphQL的另一个主要用例是将多个现有系统集成在一个统一的GraphQL API后面。这对有着遗留基础设施和许多不同API的公司尤其引人注目,这些API已经维护了多年,现在却造成了很重的维护负担。这些传统系统的一个主要问题是,人们很难做出需要访问多个旧系统的创新产品。

在这种情况下,GraphQL可以用于统一这些系统,并将这些复杂的事情隐藏在一个友好的GraphQL API之后。这样,就可以去开发新的客户端应用,只需与GraphQL服务器通信即可获取所需的数据。然后,GraphQL服务器负责从现有系统中提取数据,并以GraphQL格式进行响应。

就像在以前的架构中,GraphQL服务器不关心正在使用的数据库的类型,这一次它不关心用来获取查询结果所需的数据源。

图片描述

GraphQL允许你隐藏现有系统的复杂性,在统一的GraphQL接口后面,集成微服务,传统基础架构和第三方API。

3.连接数据库与现有系统集成的混合方式

最后,可以组合这两种方法,构建一个连接数据库,同时又与旧有或第三方系统集成的GraphQL服务器。

当服务器接收到查询时,将解析它,并从连接的数据库或某些集成的API中检索出所需的数据。

图片描述

这两种方法也可以组合起来,GraphQL服务器可以从单个数据库或者现有系统获取数据,从而达到灵活性,并将所有数据管理的复杂性交给到服务器。

Resolver方法

但是,如何通过GraphQL获得这种灵活性?它是如何适配好这些差别明显的不同类型的用例?

如上一章所了解,GraphQL查询(或mutation)的payload由一组字段组成。在GraphQL服务器实现中,这些字段中的每一个实际上都对应于一个称为resolver的函数。resolver的作用就是获取对应字段的数据。

当服务端收到查询时,会调用查询中payload里字段对应的函数,检索出每个字段的数据。当所有resolver都返回了结果,服务端将按照查询里描述的数据格式打包好数据,将结果返回给客户端。

图片描述

查询中的每个字段对应一个resolver函数。当需要查询指定数据时,GraphQL将调用所有需要的resolver方法

GraphQL客户端库

GraphQL对于前端开发人员来说特别好,因为它完全消除了REST API的许多不便和缺点,例如过度和欠缺的数据加载。复杂性被推到服务端,强大的服务器可以处理繁重的计算工作。客户端不必知道获取的数据实际来自哪里,只需要使用单一,一致且灵活的API就可以。

让我们考虑使用GraphQL引入的重大变化,从一个相当迫切的数据获取方法转变为一个纯粹的声明式方法。从REST API获取数据时,大多数应用必须执行以下步骤:

1.构造和发送HTTP请求(例如,在Javascript中fetch
2.接收并解析服务器响应
3.在本地存储数据(简单地存在内存或持久存储)
4.在UI中显示数据

使用理想化的声明式数据获取方法,客户端不应该做以下两个步骤:

1.描述数据要求
2.在UI中显示数据

所有底层网络任务以及数据存储都应该被抽象出来,数据依赖关系的声明才应该是主要部分。

这恰恰是GraphQL的客户端库,如Relay或Apollo能做到的。它们提供了重要部分的抽象,让你不必去重复执行基础方法。从而让你能够专注于应用本身。

进阶 - 1.客户端

在前端使用GraphQL API,对于抽象和实现基础功能,是一个好机会。让我们考虑你在应用中可能想要的一些“基础”功能:

  • 直接发送查询和mutation而不用构建HTTP请求
  • 视图层集成
  • 缓存
  • 基于schema去校验和优化查询

当然,没有什么可以阻止你仅使用HTTP来获取你的数据,然后自己逐个处理,直到正确的信息最终显示在UI上。 但是GraphQL提供了避免大量手动劳动的能力,让你专注于应用的核心部分! 在下文中,我们将更详细地讨论这些能力。

目前有两个主要的GraphQL客户端库。 第一个是Apollo客户端,这是一个社区支持的项目,旨在为所有主要开发平台构建强大而灵活的GraphQL客户端。 第二个被称为Relay,它是Facebook官方的GraphQL客户端,它大大优化了性能,但只能在web上可用。

直接发送查询和mutation

GraphQL的主要优点是它允许您以声明的方式获取和更新数据。换句话说,我们在API抽象层面更进一步,不必再自己处理低级网络任务。

以前你使用纯HTTP(如JavaScript中的fetch或iOS中的NSURLSession)从API加载数据。使用GraphQL后,所有操作都将写入一个查询,声明了数据需求后,让系统负责发送请求并处理响应。这正是GraphQL客户端要做的。

视图层集成和UI更新

在服务端的响应,被GraphQL客户端接收到之后,数据需要最终展现在UI中。根据开发的平台和选用的框架不同,UI更新也将会有不同的方式。

以React为例,GraphQL客户端使用高阶组件的理念来获取所需的数据,并使其在组件的porps中可用。一般来说,GraphQL的声明式特性与函数式反应型编程(FRP)结合的很好。两者可以形成强大的组合,其中视图只是声明其数据依赖,UI则与选择的FRP层连接。

缓存查询结果:概念和策略

在大多数应用中,你需要缓存之前从服务器获取的数据。这对于提供流畅的用户体验和维护好用户数据至关重要。

通常,当缓存数据时,直觉是将远程获取的信息放入本地存储中,以便稍后检索。使用GraphQL,直觉上的办法就是将GraphQL查询的结果简单地放入存储,并且只要再次执行完全相同的查询,就返回先前存储的数据。事实证明,这种方式对于大多数应用来说是非常低效的。

更有效的方式是提前规范化数据。这意味着(可能是)嵌套的查询结果会变得平坦,并且存储的是,可以使用全局唯一ID来查询的单个记录内容。如果想了解更多关于这一点的信息,Apollo博客对这个内容做了很好的介绍。

构建时的验证和优化

由于视图(schema)包含有关客户端可以使用GraphQL API的所有信息,因此,客户端在构建时验证查询就变得很方便。

当构建环境可以访问schema时,它可以基本解析位于项目中的所有GraphQL代码,并将其与schema中的信息进行比较。这样就可以捕获打字错误和其他错误,避免一些严重后果。

结合视图层和数据层

GraphQL的一个强大的理念是,它允许你并行地处理UI代码和数据需求。界面和数据的紧密结合,大大提高了开发人员的体验。不用再去考虑,如何在UI中恰当的填充数据。

结合带来的优势大小,取决于您正在开发的平台。例如在Javascript应用中,可以将数据依赖和UI代码放在同一个文件中。在Xcode中,Assistant Editor可用于同时处理视图控制器和graphql代码。

进阶 - 2.服务端

GraphQL通常被认为是前端的API技术,因为它使客户端能够以更好的方式获取数据。 但是既然是API,当然是在服务器端实现的。 因为GraphQL使服务器开发人员能够专注于描述数据,而不是实现和优化特定的接口,所以在服务器上也有很多好处。

GraphQL执行

GraphQL通过定义Schema和查询语言,来从Schema中检索数据,但却不仅仅是这一种方式。更是一种实际的执行算法,用于将这些查询转换为结果。 该算法的核心非常简单:查询逐字段遍历,为每个字段执行“解析器”。 让假设我们有以下模式:

type Query {
  author(id: ID!): [Author]
}

type Author {
  posts: [Post]
}

type Post {
  title: String
  content: String
}
复制代码

下面是我们使用该Schema发送到服务器的查询:

query {
  author(id: "abc") {
    posts {
      title
      content
    }
  }
}
复制代码

首先要关注的是查询中的每个字段都可以与一个类型相对应:

query: Query {
  author(id: "abc"): Author {
    posts: [Post] {
      title: String
      content: String
    }
  }
}
复制代码

现在,我们可以轻松地找到并运行服务器中每个字段对应的解析器。从查询类型开始执行,并以外层为先。这意味着我们先运行Query.author的解析器。然后,我们将该解析器的结果传递给它的子解析器,Author.posts的解析器。在下一级,结果是一个列表,在这种情况下,算法会依次在每个元素上执行一次。所以最终执行工作如下:

Query.author(root, { id: 'abc' }, context) -> author
Author.posts(author, null, context) -> posts
for each post in posts
  Post.title(post, null, context) -> title
  Post.content(post, null, context) -> content
复制代码

最终,执行算法将所有结果数据正确的放在定义好的结构中,并返回。
需要注意的是,大多数GraphQL服务器实现将提供“默认解析器” - 因此您不必为每个单个字段指定解析器函数。例如,在GraphQL.js中,当解析器的父对象包含具有正确名称的字段时,不需要指定解析器。
在Apollo博客上的“GraphQL Explained“文章中,可更深入的了解GraphQL执行情况。

批量解析

你可能会注意到上述执行策略的一件事是,它有点幼稚。例如,如果你有从后端API或数据库提取的解析器,则在执行一个查询期间可能会多次调用该后端。让我们假设,我们想获取几个帖子的作者,就像这样:

query {
  posts {
    title
    author {
      name
      avatar
    }
  }
}
复制代码

如果这些是博客上的帖子,很可能很多帖子将有相同的作者。所以如果我们需要一个API调用来获取每个作者对象,我们可能会意外地为同一个对象发出多个请求。例如:

fetch('/authors/1')
fetch('/authors/2')
fetch('/authors/1')
fetch('/authors/2')
fetch('/authors/1')
fetch('/authors/2')
复制代码

我们如何解决这个问题?让我们聪明一点。我们可以将fetch函数封装在一个工具函数中,该实函数将等待所有的解析器运行后,再确保只fetch每个元素一次:

authorLoader = new AuthorLoader()

// Queue up a bunch of fetches
authorLoader.load(1);
authorLoader.load(2);
authorLoader.load(1);
authorLoader.load(2);

// Then, the loader only does the minimal amount of work
fetch('/authors/1');
fetch('/authors/2');
复制代码

我们能做得更好吗?当然,如果我们的API支持批量请求,我们只能对后端执行一次提取操作,如下所示:

fetch('/authors?ids=1,2')
复制代码

这也可以封装在上面的工具函数中。
在JavaScript中,可以使用 DataLoader 的工具实现上述策略,其他语言也有类似的工具。

进阶 - 3.工具和生态系统

您可能已经意识到,GraphQL生态系统正在以惊人的速度增长。之所以如此,原因之一是GraphQL使我们很容易开发出优秀的工具。在本节中,我们将看到为什么会这样,以及已经存在生态系统中有的一些惊人的工具。

如果您熟悉GraphQL基础知识,您可能知道GraphQL的类型系统如何帮助我们快速定义API的最外层。它允许开发人员清楚地定义API的功能,还可以根据Schema去验证传入的查询内容。

GraphQL神奇的是,这些功能不仅仅是服务器所知道的。GraphQL允许客户端向服务器询问其Schema的信息。 GraphQL调用这个introspection。

introspection

模式的设计者已经知道模式是什么样的,但客户端如何得知,可以通过GraphQL API访问哪些数据?我们可以通过查询__schema元字段来询问GraphQL,该元字段始终在符合规范的查询根类型上可用。

query {
  __schema {
    types {
      name
    }
  }
}
复制代码

以此Schema为例:

type Query {
  author(id: ID!): Author
}

type Author {
  posts: [Post!]!
}

type Post {
  title: String!
}
复制代码

如果我们发送上述的introspection查询,我们会得到以下结果:

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Query"
        },
        {
          "name": "Author"
        },
        {
          "name": "Post"
        },
        {
          "name": "ID"
        },
        {
          "name": "String"
        },
        {
          "name": "__Schema"
        },
        {
          "name": "__Type"
        },
        {
          "name": "__TypeKind"
        },
        {
          "name": "__Field"
        },
        {
          "name": "__InputValue"
        },
        {
          "name": "__EnumValue"
        },
        {
          "name": "__Directive"
        },
        {
          "name": "__DirectiveLocation"
        }
      ]
    }
  }
}
复制代码

我们可以看到,我们查询了Schema上的所有类型。我们得到我们定义的对象类型和预定义类型。我们甚至可以再查询introspection类型!

对于introspection类型,不仅仅能拿到名字。看下面的例子:

{
  __type(name: "Author") {
    name
    description
  }
}
复制代码

在这个例子中,我们使用__type元字段来查询一个类型,我们得到它的名字和描述。此查询的结果:

{
  "data": {
    "__type": {
      "name": "Author",
      "description": "The author of a post.",
    }
  }
}
复制代码

如你所见,introspection是GraphQL非常强大的功能,我们只是了解了一点皮毛。在规范中,会详细介绍introspection模式中哪些字段和类型是可用的。

GraphQL生态系统中的许多工具都是通过introspection系统提供了神奇的功能。例如文档浏览器,自动补全,代码生成,和其他一切可能!在构建和使用GraphQL API时,当你重度使用introspection ,最有用的工具是GraphiQL。

GraphiQL

GraphiQL是用于编写,验证和测试GraphQL查询的运行在浏览器中的IDE。它有用于GraphQL查询的编辑器,配有自动补全和验证以及文档浏览,可以快速呈现出模式的结构(由introspection提供)。

这是一个非常强大的开发工具。它允许您在GraphQL服务器上调试和尝试查询,而无需通过curl去写GraphQL查询。

试一试吧! graphql.org/swapi-grap.…

进阶 - 4.安全

GraphQL为客户端提供强大的能力。但是拥有强大的能力时,也会带来更大的风险。

由于客户端有可能使用非常复杂的查询,因此我们的服务器必须能够妥善处理。这些查询可能是来自恶意客户端的滥用查询,或者可能只是合法客户端使用的非常大的查询。在这两种情况下,客户端可能会将您的GraphQL服务器崩溃。

我们将在本章中介绍一些减轻这些风险的策略。我们将以最简单到最复杂的顺序来说明,并看看这些方式的利弊。

超时策略

第一个策略,也是最简单的策略是使用简单的超时来防范大型查询。这不需要服务器了解有关传入查询的任何内容。服务器需要知道的仅仅是允许查询的最长时间。

例如,配置了5秒超时的服务器将停止执行超过5秒钟执行的任何查询。

超时的优势

  • 操作简单
  • 大多数策略都会使用超时作为最终保护

超时的缺点

  • 即使有超时策略,也可能会造成不好的后果
  • 有时难以实施。在一段时间之后切断连接可能会导致奇怪的行为。

最大查询深度

正如我们之前所述,使用GraphQL的客户可以随意写出任意的复杂查询。由于GraphQL模式通常是嵌套的,这意味着客户端可以写出如下所示的查询:

query IAmEvil {
  author(id: "abc") {
    posts {
      author {
        posts {
          author {
            posts {
              author {
                # that could go on as deep as the client wants!
              }
            }
          }
        }
      }
    }
  }
}
复制代码

如果我们可以阻止客户滥用这样的查询深度呢? 在了解定义的模式时,可以让你了解合法查询的深度。这实际上是可以实现的,并且通常称为最大查询深度。

通过分析查询文档的AST,GraphQL服务器能够根据其深度拒绝或接受请求。

例如,配置了最大查询深度为3的服务器,以及以下查询文档。红色方框选中的所有内容都被认为深度太深,查询无效。

图片描述

使用最大查询深度设置的graphql-ruby服务,我们得到以下返回结果:

{
  "errors": [
    {
      "message": "Query has depth of 6, which exceeds max depth of 3"
    }
  ]
}
复制代码

最大查询深度优点

  • 由于静态分析了文档的AST,因此查询甚至不执行,所以不会在GraphQL服务器上增加负担。

最大查询深度缺点

  • 只有深度往往不足以涵盖所有滥用查询。 例如,在根节点上请求大量的查询将是代价巨大的,但不太可能被查询深度分析器阻止。

查询复杂性

有时,查询的深度还不足以真正了解GraphQL查询的开销。在很多情况下,我们的模式中的某些字段比其他字段更复杂。

查询复杂性允许您定义这些字段的复杂程度,并限制最大复杂度的查询。这个想法是通过使用一个简单的数字来定义每个字段的复杂程度。一个常见的默认设置是给每个字段一个复杂的1。以这个查询为例:

query {
  author(id: "abc") { # complexity: 1
    posts {           # complexity: 1
      title           # complexity: 1
    }
  }
}
复制代码

一个简单的加法,告诉我们查询的复杂性是3。如果我们在我们的架构上设置最大复杂度为2,则此查询将会失败。

如果posts字段实际上比作者字段复杂度高很多呢?我们可以为该领域设置不同的复杂性。我们甚至可以根据参数设置不同的复杂性! 我们来看看一个类似的查询,其中posts会根据传入的参数去确定复杂性:

query {
  author(id: "abc") {    # complexity: 1
    posts(first: 5) {    # complexity: 5
      title              # complexity: 1
    }
  }
}
复制代码

查询复杂性的优点

  • 可以覆盖比更多的用例。
  • 通过静态分析复杂性,在执行前拒绝查询。

查询复杂性缺点

  • 很难实现完美
  • 如果需要开发时预估复杂性,我们如何保持状态最新?我们一开始怎么能知道查询成本?
  • Mutations 很难估计。如果他们有一个难以衡量的附加操作,如在后台排队执行的任务怎么办?

节流

到目前为止,我们看到的解决方案都是会阻止滥用服务器的查询。像这样使用它们的问题是,它们会阻止大量查询,但不会阻止客户端生成出大量查询!

在大多数API中,使用简单的节流方式是,阻止客户端频繁地请求资源。GraphQL有点特别,因为调节请求数并没有真正帮助我们。即使是很少的请求也可能是大量的查询。

事实上,我们不知道客户端定义了多少请求是可以接受的。那么我们如何来限制客户端呢?

基于服务器执行时间的调节

我们可以通过查询执行时的服务器耗时,来估计查询的复杂程度。我们可以使用这种式来限制查询。凭借对系统的了解,您可以提出客户端可以在特定时间范围内使用的最大服务器时间。

我们还决定随着时间的推移,客户端添加多少服务器时间。这是一个经典的leaky bucket 算法。请注意,还有其他节流算法,但这些算法超出了本章的范围。在下面的例子中我们将使用leaky bucket。

让我们想象一下,我们将允许的最大服务器时间(Bucket Size)设置为1000ms,客户端每秒获得100ms的服务器时间(Leak Rate),mutation 如下:

mutation {
  createPost(input: { title: "GraphQL Security" }) {
    post {
      title
    }
  }
}
复制代码

这个mutation平均需要200ms才能完成。实际上,时间可能会有所不同,但我们假设为了这个例子,它总是需要200ms才能完成。

这意味着在1秒内调用此操作超过5次的客户端将被阻止,直到更多的可用服务器时间添加到客户端。

经过两秒钟(100ms加秒),我们的客户可以一次调用createPost。

正如你所看到的,基于时间的调节是限制GraphQL查询不错的方式,因为复杂的查询将最终消耗更多的时间,这意味着你不能频繁地调用它们,而较小的查询可能被更频繁地调用,因为它们将非常快速地计算。

但如果GraphQL API是公开的,向客户端提出这些限制条件就不那么容易了。在这种情况下,服务器耗时并不能很好地告知客户端,客户端也不能准确的估计他们的查询所需要的时间,在不先试着请求的情况下。

还记得我们之前提到的最大复杂度?如果我们根据这个调节,会怎么样?

基于查询复杂度的调节

基于查询复杂度的调节是与客户端合作的好方法,客户端可以遵循schema中的限制。

我们使用与“查询复杂性”部分中使用的相同的复杂性示例:

query {
  author(id: "abc") {    # complexity: 1
    posts {              # complexity: 1
      title              # complexity: 1
    }
  }
}
复制代码

我们知道这个查询的成本是基于复杂度的3。就像时间流逝一样,我们可以得知客户可以使用的每次最高成本(Bucket Size)。

如果最大成本为9,我们的客户只能三次运行此查询,不允许查询更多。

这些原理与我们的时间节制相同,但现在将这些限制传达给客户端后。客户甚至可以自己计算查询成本,而无需估计服务器时间!

GitHub公共API实际上使用这种方法来扼制客户端。看看他们如何对用户表达这些限制:https://developer.github.com/v4/guides/resource-limitations/。

总结

GraphQL非常适合用于客户端,因为给予了更多的功能。但是,强大的功能也带来了风险,担心客户端会以非常昂贵的查询来滥用GraphQL服务器。

有许多方法来保护您的GraphQL服务器免受这些查询,但是它们都不是万无一失的。重要的是,我们要知道有哪些方法可用来限制,并了解他们的优缺点,然后采取最优的决定!

进阶 - 5.常见问题

GraphQL是数据库技术吗?

不是。GraphQL经常与数据库技术混淆。这是一个误解,GraphQL是API的查询语言,而不是数据库。在这个意义上,它是数据库无关的,可以用于任何类型的数据库,甚至根本没有数据库。

GraphQL仅适用于React / Javascript开发人员?

GraphQL是一种API技术,因此可以在需要API的任何上下文中使用。

在后端,GraphQL服务器可以用任何可用于构建Web服务器的编程语言实现。在JavaScript之外,Ruby,Python,Scala,Java,Clojure,Go和.NET都有流行的实现可以参考。

由于GraphQL API通常通过HTTP进行操作,任何可以发起HTTP请求的客户端都可以从GraphQL服务器查询数据。

注意:GraphQL实际上是传输层不可知的,所以您可以选择其他协议,比HTTP来实现您的服务器。

如何做服务器端缓存?

GraphQL的一个常见问题,特别是与REST进行比较时,难以维护服务器端缓存。使用REST,可以轻松地为每个端点缓存数据,因为它确保数据的结构不会改变。

另一方面,使用GraphQL,客户端下一步要求什么不清楚,所以将缓存层放在API的后面并没有什么意义。

服务器端缓存仍然是GraphQL的挑战。有关缓存的更多信息可以在GraphQL网站上找到。

如何进行身份验证和授权?

认证和授权往往是混淆的。身份验证描述了声明身份的过程。这是当您使用用户名和密码登录服务时,您进行身份验证。另一方面,授权描述了指定个别用户和用户组对系统某些部分的访问权限的权限规则。

GraphQL中的身份验证可以使用诸如OAuth的常用模式来实现。

为了实现授权,建议将任何数据访问逻辑委派给业务逻辑层,而不是直接在GraphQL实现中处理它。如果您想要了解如何实施授权的灵感,可以查看Graphcool的权限查询。

如何处理错误?

一个成功的GraphQL查询应该返回一个名为“data”的根字段的JSON对象。如果请求失败或部分失败(例如因为请求数据的用户没有正确的访问权限),则将一个称为“errors”的第二根字段添加到响应中:

{
  "data": { ... },
  "errors": [ ... ]
}
复制代码

有关更多详细信息,可参考GraphQL规范

GraphQL是否支持脱机使用?

GraphQL是(web)API的查询语言,在这个意义上,只能在线工作。 但是,客户端的脱机支持是一个有意义的问题。 Relay和Apollo的缓存能力对于一些用例可能已经足够了,但目前,还没有一个流行的解决方案来存储数据。你可以在Relay和Apollo的GitHub问题中获得更多见解,其中讨论了关于脱机的支持。

离线使用和持久性的一个有趣的方法可以在这里找到。

复制代码