第3章
GraphQL查询语言
在距GraphQL开源45年前,一位名叫埃德加 "科德(Edgar M. Codd)的IBM程序员发表了一篇内容不多、名字却很长的论文—“A Relational Model of Data for Large Shared Databanks”。此文虽然标题平淡无奇,其中表达的思想却惊天动地。它概述了使用表格存储和操作数据的模型。此后不久,IBM开始开发可以使用结构化英文查询语言(Structured English Query Language,SEQUEL)进行查询的关系型数据库,这就是后来的SQL。
SQL(Structured Query Language,结构化查询语言)是特定的语言,用于访问、管理和操作数据库中的数据。SQL引入了使用单个命令访问多个记录的概念。它还可以使用任意键(key)来访问记录,而不仅局限于ID。
SQL可以运行的命令非常精简:SELECT、INSERT、UPDATE和DELETE。这就是你对数据所能做的一切。使用SQL,可以编写一个查询语句,它可以跨数据库中的多个数据表返回相应的数据。
数据只能被增加、删除、更改和查询的想法确实符合表述性状态传递方式,它要求我们根据这四种基本数据操作使用不同的HTTP方法:GET、POST、PUT和DELETE。但是,指定通过REST读取或更改数据类型的唯一方法是通过端点URL,而非实际的查询语言。
最初我们开发GraphQL的想法是用于查询数据库并将其应用于互联网。单次的GraphQL查询就可以返回相关联的数据。与SQL一样,你可以使用GraphQL查询来更改或删除数据。毕竟,SQL中的QL和GraphQL中的QL代表了相同的东西:查询语言(Query Language)。
尽管都是查询语言,但GraphQL和SQL却完全不同。它们适用于完全不同的环境。我们将SQL查询语句发到数据库,而GraphQL查询语句则发到API。SQL数据存储在数据表中。GraphQL数据则可以存储在任何地方:单个数据库、多个数据库、文件系统、RESTful API、WebSockets,甚至是其他GraphQL API。SQL是一种数据库的查询语言,而GraphQL则是一种互联网查询语言。
GraphQL和SQL的语法也完全不同。GraphQL使用查询(Query)字段去请求数据而非SELECT。查询字段是我们使用GraphQL操作的核心。不同于INSERT、UPDATE或者DELETE,GraphQL将所有这些数据整合成为一种数据类型:变更(Mutation)。因为GraphQL是为互联网而生的,所以它具有订阅功能,可以用来监听socket连接上的数据变动。SQL可不具备订阅功能。
GraphQL是依照规范进行标准化的。使用哪种语言并不重要:只要是GraphQL查询字段就行了。无论你的项目使用的是JavaScript、Java、Haskell或是其他什么语言,查询字段基本一致。
查询字段仅仅是发送到GraphQL端的POST请求体中的字符串。下面是一个普通的GraphQL查询字段:
{
allLifts {
name
}
}
你可以使用curl将该查询字段发送到GraphQL端:
curl 'http://snowtooth.herokuapp.com/'
-H 'Content-Type: application/json'
--data '{"query":"{ allLifts {name }}"}'
假设GraphQL schema支持这种形式的查询,你将直接收到响应的JSON。JSON中包含你在名为data的字段中请求的数据,或者出错的时候返回的报错信息。流程很简单,发出请求,收到响应。
如果要更改数据,我们可以发送变更(mutation)。变更和查询字段颇为相似,而它是用来修改数据的,类似于SQL的UPDATE。执行更改所需要的数据可以和变更一起直接发送,请看下面的例子:
mutation {
setLiftStatus(id: "panorama" status: OPEN) {
name
status
}
}
上面的变更是采用GraphQL查询语言编写的,可以假设它想要把id为panorama的lift的状态改为OPEN。同样,可以使用curl将该操作发送到GraphQL服务器:
curl 'http://snowtooth.herokuapp.com/'
-H 'Content-Type: application/json'
--data '{"query":"mutation {setLiftStatus(id: "panorama" status: OPEN) {name status}}"}'
随后将详细介绍将变量映射到查询或变更中的更好的方法。在本章中,我们将重点介绍如何使用GraphQL来构成查询(query)、变更(mutation)和订阅(subscription)。
GraphQL API工具
GraphQL社区已经开发了几个和GraphQL API交互的开源工具。它们可以帮助你通过GraphQL查询语言编写查询,发送到GraphQL端获取返回的JSON。下面我们将介绍两种最常用的针对GraphQL API的GraphQL查询字段测试工具:GraphiQL和GraphQL Playground。
GraphiQL
GraphiQL是由Facebook创建的浏览器内的集成开发环境(IDE),方便开发人员查询和浏览GraphQL API。GraphiQL提供语法高亮显示、代码完成和错误警告,它使你可以在浏览器中直接运行并查看查询结果。许多公共API提供了GraphiQL接口,你可以使用该接口查询实时数据。
它的操作界面相当简单。在其中编写查询,点击运行按钮便可在右侧面板中看到反馈结果,如图3-1所示。
我们的查询流程从使用GraphQL查询语言编写的文本开始。我们将此文本称为查询文档。将查询文本写入左侧面板。在GraphQL文档中可以定义一个或多个操作,比如查询、变更或是订阅。图3-2显示了如何向文档中添加查询操作。
单击播放按钮进行查询。在右侧面板中会将到JSON格式的响应结果(见图3-3)。
点击右上角打开Docs窗口,这里显示了与当前服务交互所需了解的全部内容。该文档会自动添加到GraphiQL,因为它是从服务的schema中读取的。schema定义了服务上可用的数据,并且GraphiQL通过对schema进行自检查询来自动构建文档。在此期间,你始终可以在文档资源管理器中查看它,如图3-4所示。
通常来说,你是通过GraphQL服务主机本身的URL来访问GraphiQL的,如果你构建自己的GraphQL服务,那么可以增加一个路由来渲染GraphQL接口,以便你的用户可以浏览公开的数据。当然,你还可以下载独立版本的GraphiQL。
GraphQL Playground
另一个浏览GraphQL API的工具是GraphQL Playground。由Prisma团队开发的GraphQL Playground除了具有GraphiQL的功能之外,还添加了一些有趣的配置项。访问https://www.graphqlbin.com ,可以最快体验GraphQL Playground。只要你提供接口,就可以通过它和数据进行交互。
GraphQL Playground和GraphiQL非常相似,并且还有一些让你用起来很方便的额外功能。其中最重要的就是可以通过GraphQL请求发送自定义的HTTP请求头,如图3-5所示(我们将在第5章介绍授权时更详细地讨论该功能)。
GraphQL Bin也是一款很方便的协作工具,你可以和其他人共享你的Bin链接,见图3-6。
GraphQL Playground有桌面版本,你可以使用Homebrew在本地安装:
brew cask install graphql-playground
或者你可以直接从网站http://bit.ly/graphql-pg-releases 下载。
在安装或了解了GraphQL Bin后,就可以开始发送查询(Query)了。要快速上手,可直接在Playground网站上粘贴API(公共API或本地运行的localhost项目均可)。
公共GraphQL API
从零开始练习GraphQL的最好办法就是使用公共API来发送查询。有一些公司提供了GraphiQL接口,你可以使用它来查询公共数据:
SWAPI (https://graphql.github.io/swapi-graphql/ )
这是本书开头曾经提过的Facebook项目—星球大战API。
GitHub API (https://developer.github.com/v4/explorer/ )
作为最大的公共GraphQL API之一,GitHub GraphQL API允许你发送查询和变更,以查看和更改GitHub上的实时数据。当然,你需要登录GitHub才能享受该服务。
Yelp (https://www.yelp.com/login?return_url=%2Fdevelopers%2Fgraphiql )
Yelp维护了一个可以使用GraphiQL查询的GraphQL API。当然你需要创建一个Yelp开发账号才行。
更多公共GraphQL API的示例参见(https://github.com/APIs-guru/graphql-apis )。
GraphQL查询字段
假定有一个名为Snowtooth的滑雪胜地,这里的Web团队正在使用GraphQL提供关于缆车和雪道状态的实时信息。雪地摩托巡逻队可以通过手机直接开关缆车和雪道。本章的示例请参考Snowtooth的GraphQL Playground接口(http://snowtooth.moonhighway.com )。
你可以使用查询操作从API请求数据。查询字段就是你希望从GraphQL服务器获取的数据。在发送查询时,会按字段请求数据。你从服务器接收到的JSON数据响应中的相同字段会自动进行映射。例如,如果你发送allLifts查询字段请求名称和状态,那么便会收到一个JSON响应,其中包含allLifts的数组以及每个电梯name(名称)和status(状态)的对象字符串,如下所示:
query {
allLifts {
name
status
}
}
错误处理
成功的查询将返回一个JSON文档,它会包含“data”键。如果查询失败,则会返回一个包含“errors”键的JSON文档,错误信息会作为值。JSON中是可以同时包含“data”键和“errors”键的。
你可以向查询文档添加多个查询,但是一次只能运行一个操作。例如,你可以在查询文档中放置两个查询操作。
query lifts {
allLifts {
name
status
}
}
query trails {
allTrails {
name
difficulty
}
}
当按下“play”按钮时,GraphQL Playground将会要求你在两个操作之间选择一个。如果你想在一个请求中查询所有数据,那么就需要将全部字段放在同一个查询中:
query liftsAndTrails {
liftCount(status: OPEN)
allLifts {
name
status
}
allTrails {
name
difficulty
}
}
这就是GraphQL开始展现优势的时候。可以在一次查询中接收各种不同的数据。我们通过status: OPEN查询状态为开启的缆车的数量liftCount,它为我们提供了相应的数据。我们还可以查询每个电梯的name和status。最后查询每条雪道的name和difficulty。
Query是GraphQL类型。我们称其为根类型(root type),因为它可以将操作进行映射,而操作代表了查询文档的根节点。我们已经在API schema中定义了可以查询的GraphQL字段。文档将告诉我们可以在查询类型上选择哪些字段。
根据文档,我们了解到当查询此API时,可以选择的字段有liftCount、allLifts和allTrails。它也定义了更多字段可供选择,但是查询的关键就在于我们可以自主选择需要哪些字段以及省略哪些字段。
编写查询时,把需要的字段用大括号括起来。这些块被称为选择集(selection set)。在选择集内定义的字段和GraphQL类型直接相关。查询类型中已经定义了liftCount、allLifts和allTrails。
选择集之间可以相互嵌套。因为allLifts字段返回了Lift类型的列表,所以我们需要使用大括号以在此类型上创建新的选择集。与缆车相关的各种数据都是可请求的,在此例中我们仅请求缆车的name和status。同样,allTrails查询字段也返回对应的Trail类型。
返回的JSON包括了我们在查询中请求的所有数据。数据的格式和我们的查询格式一致。每个JSON字段的名称也和我们选择集中的字段相同。可以通过指定别名来更改查询结果中返回对象的字段名称,如下所示:
query liftsAndTrails {
open: liftCount(status: OPEN)
chairlifts: allLifts {
liftName: name
status
}
skiSlopes: allTrails {
name
difficulty
}
}
响应如下所示:
{
"data": {
"open": 5,
"chairlifts": [
{
"liftName": "Astra Express",
"status": "open"
}
],
"skiSlopes": [
{
"name": "Ditch of Doom",
"difficulty": "intermediate"
}
]
}
}
现在我们把数据恢复到相同的结构,然后把其中几个字段重新命名。传入查询参数(query argument)可对GraphQL查询结果进行筛选。参数是与查询相关联的键值对。如果我们想要获知处于关闭状态的缆车的名字,可以发送一个参数来过滤响应:
query closedLifts {
allLifts(status: "CLOSED", sortBy: "name") {
name
status
}
}
还可以使用参数来选择数据。举例来说,假设我们想要查询单个缆车的状态,那么可以传入唯一标识符:
query jazzCatStatus {
Lift(id: "jazz-cat") {
name
status
night
elevationGain
}
}
这样我们就能够看到返回的“Jazz Cat”缆车数据,包括name(名称)、status(状态)、night(夜间运行)和elevationGain(爬高)。
边和连接
在GraphQL查询语言中,字段可以是标量类型或对象类型。标量类型与其他语言中的基本类型类似,是选择集的“叶子”,开箱即用,GraphQL带有5种内置的标量类型:整数(Int)、浮点数(Float)、字符串(String)、布尔值(Boolean)和唯一标识符(ID)。整数和浮点数都返回JSON数字,字符串和ID 都返回JSON字符串。布尔值类型只返回布尔值。尽管ID和String将返回相同类型的JSON数据,但是GraphQL仍然确保ID返回唯一的字符串。
GraphQL对象类型是schema中定义的一个或多个字段的组,定义了应当返回的JSON对象的形式。JSON可以在字段下无限嵌套对象,GraphQL也可以。可以通过查询一个对象和相关对象的详细信息将对象连接在一起。
例如,假设我们想要一份雪道列表,以便乘坐指定的缆车:
query trailsAccessedByJazzCat {
Lift(id:"jazz-cat") {
capacity
trailAccess {
name
difficulty
}
}
}
在前面的查询中,需要一些关于“Jazz-Cat”缆车的数据。我们的选择集包括了对capacity字段的请求。capacity是标量类型,它返回一个整数,该整数表示一次可以乘坐的人数。trailAccess字段的类型是Trail(对象类型)。在这个例子中,trailAccess返回的是一份经过筛选的缆车列表,可通过jazz-cat进行访问。由于trailAccess是Lift类型中的一个字段,因此API可以使用父对象jazz-cat Lift的详细信息对缆车列表进行筛选。
这个操作查询了两种类型数据(lifts和trails)之间的一对多连接。一台缆车和许多雪道相关联。如果从Lift节点开始遍历图形,便可访问到一个或多个Trail节点,这些节点通过一条名为trailAccess的边连接到Lift节点。如果有人觉得这里的图是无向图,那么我们就从Trail节点遍历到Lift节点,如下所示:
query liftToAccessTrail {
Trail(id:"dance-fight") {
groomed
accessedByLifts {
name
capacity
}
}
}
在liftToAccessTrail查询中,我们选择了一条名为dance-fight的雪道(Trail)。groomed字段返回了一个布尔值标量类型,我们可以据此判断缆车是否正在维护。而accessedByLifts字段返回了正在将滑雪爱好者送往dance-fight雪道的缆车。
片段
GraphQL查询文档可以定义操作和片段。片段(fragment)是可以在多个操作中重用的选择集,如下所示:
query {
Lift(id: "jazz-cat") {
name
status
capacity
night
elevationGain
trailAccess {
name
difficulty
}
}
Trail(id: "river-run") {
name
difficulty
accessedByLifts {
name
status
capacity
night
elevationGain
}
}
}
这次查询请求jazz-cat缆车和river-run雪道的问题。该缆车的选择集内包括了name、status、capacity、night和elevationGain字段。我们期望获取的river-run雪道的信息和缆车类型有重复的部分。这样一来,我们便能够创建一个片段来减少查询中的冗余:
fragment liftInfo on Lift {
name
status
capacity
night
elevationGain
}
你可以使用fragment(片段)标识符来创建片段。片段是针对特定类型的选择集,因此必须在其定义中包含和每个片段相关联的类型。本例中的片段名为liftInfo,它是Lift类型上的一个选择集。
当我们想要将liftInfo片段字段添加到另一个选择集时,可以通过三点语法和片段名称来实现,具体代码如下:
query {
Lift(id: "jazz-cat") {
...liftInfo
trailAccess {
name
difficulty
}
}
Trail(id: "river-run") {
name
difficulty
accessedByLifts {
...liftInfo
}
}
}
这个语法和ES6的扩展运算符相似,目的也差不多,都是将一个对象的键和值分配给另一个对象。三点语法表示将片段中的字段分配给当前选择集。在本例中,我们可以使用一个片段在查询中的两个不同位置选择name、status、capacity、night和elevationGain。
我们无法将liftInfo片段添加到Trail选择集中,因为它只定义了Lift类型上的字段。我们可以为Trail添加另一个片段:
query {
Lift(id: "jazz-cat") {
...liftInfo
trailAccess {
...trailInfo
}
}
Trail(id: "river-run") {
...trailInfo
groomed
trees
night
}
}
fragment trailInfo on Trail {
name
difficulty
accessedByLifts {
...liftInfo
}
}
fragment liftInfo on Lift {
name
status
capacity
night
elevationGain
}
在这个例子中,我们创建了一个名为trailInfo的片段,并在两个查询中进行了引用。我们还使用了trailInfo片段中的liftInfo片段来选择与缆车相关的详细信息。你可以根据需要创建任意数量的片段并交替使用它们。在river-run雪道查询所使用的选择集当中,我们把片段同与river-run雪道相关的其他细节结合了起来。你既可以把片段和选择集当中的其他字段组合使用,也可将同一类型的多个片段组合在一个选择集当中:
query {
allTrails {
...trailStatus
...trailDetails
}
}
fragment trailStatus on Trail {
name
status
}
fragment trailDetails on Trail {
groomed
trees
night
}
使用片段的一个好处就是,可以通过修改一个片段来同时修改许多个用于不同查询的选择集:
fragment liftInfo on Lift {
name
status
}
对liftInfo片段中选择集的更改减少了所有使用该片段的每个查询所选择的数据。
联合类型
我们已经看过如何返回对象列表,但截至目前,所返回的都是单一类型的列表。如果希望列表返回不止一种类型,那么可以创建联合类型(union type),它可以把两个不同的对象类型关联起来。
假设我们正在为大学生开发一款可以安排日程的应用程序。学生可以将锻炼身体和学习活动添加到日程中,示例网址为:https://graphqlbin.com/v2/ANgjtr 。
如果你查阅GraphQL Playground的文档,将会发现AgendaItem字段是一个联合类型,这意味着它可以返回多个类型。具体来说,它应该可以返回Workout或StudyGroup,毕竟这些是大学生日程安排的一部分。
在编写学生日程查询时,可以使用片段来定义选择哪些字段,采用字符串“on”来分别指定选择条件:
query schedule {
agenda {
...on Workout {
name
reps
}
...on StudyGroup {
name
subject
students
}
}
}
响应如下:
{
"data": {
"agenda": [
{
"name": "Comp Sci",
"subject": "Computer Science",
"students": 12
},
{
"name": "Cardio",
"reps": 100
},
{
"name": "Poets",
"subject": "English 101",
"students": 3
},
{
"name": "Math Whiz",
"subject": "Mathematics",
"students": 12
},
{
"name": "Upper Body",
"reps": 10
},
{
"name": "Lower Body",
"reps": 20
}
]
}
}
这里,我们使用的是内联片段(inline fragment)。内联片段没有名称属性,可以直接在查询中将选择集分配给特定类型。当联合字段返回不同类型的对象时,可以通过它来定义选择哪些字段。对于每个Workout,该查询都会返回它的name和reps。而对StudyGroup,则返回name、subject和students。这样返回的数据就根据查询项目的不同而有所差异了。
还可以使用具名片段去查询联合类型:
query today {
agenda {
...workout
...study
}
}
fragment workout on Workout {
name
reps
}
fragment study on StudyGroup {
name
subject
students
}
接口
在处理由单个字段返回的多个对象类型时,可以使用接口(interface)。接口是一种抽象类型,它构建的是在类似对象类型中应实现的字段列表。当另一种类型部署该接口时,它便会将来自该接口的所有字段包括进来,通常还包括一些它自己的字段。如果你想查看这个示例,请访问GraphQL Bin(https://www.graphqlbin.com/v2/yoyPfz )。
当你查看文档中的agenda字段时,可以看到它返回了ScheduleItem接口。该接口定义的字段包括:name、start和end。实现ScheduleItem接口的任何对象类型都需要部署这些字段。
文档还告诉我们,StudyGroup和Workout类型实现了此接口。这意味着我们可以放心地假设这两种类型都有name、start和end字段:
query schedule {
agenda {
name
start
end
}
}
schedule查询似乎对agenda字段是否返回多个类型毫不在意。它只需要项目的name、start和end字段,以便创建该学生的日程安排表。
在查询接口时,还可以在返回特定对象类型时使用片段来选择其他字段:
query schedule {
agenda {
name
start
end
...on Workout {
reps
}
}
}
该schedule查询已修改为当ShecduleItem为Workout时额外请求reps字段。
变更
到目前为止,我们已经讨论了很多关于读取数据的内容。查询代表了发生在GraphQL中所有数据读取的操作。那么为了写入数据,我们将会使用变更(mutation)关键字。变更和查询相类似,它拥有名称(name)属性,还拥有可以返回对象类型或标量的选择集。不同之处在于,变更对数据的更改会影响后端的数据状态。
例如,下面这种变更就属于“从删库到跑路”类型的:
mutation burnItDown {
deleteAllData
}
Mutation是一个根对象类型。API schema定义了该类型上可用的字段。前面示例中的API就是通过部署deleteAllData字段删除客户端的所有数据,并返回一个标量类型:如果返回了布尔值true,那就说明所有的数据删除成功了。反之,如果返回了false,则说明可能哪里出了问题。是否实际删除数据还是由API来处理,这一点我们将在第5章中进一步讨论。
让我们考虑另一个变更的示例,与其搞破坏,不如进行创造:
mutation createSong {
addSong(title:"No Scrubs", numberOne: true, performerName:"TLC") {
id
title
numberOne
}
}
这是一个创建新歌的变更。歌曲的title、numberOne状态和performerName作为参数发送,我们可以假设这次变更将这首新歌添加到了数据库中。如果变更字段返回一个对象,那么需要在变更的末尾添加一个选择集。在这种情况下,当变更执行完成后,将返回一个Song(歌曲)类型,包括刚刚创建的歌曲详细信息,带有id、title、numberOne字段,如下所示:
{
"data": {
"addSong": {
"id": "5aca534f4bb1de07cb6d73ae",
"title": "No Scrubs",
"numberOne": true
}
}
}
之前的例子展示了变更操作可能产生的反应。如果出现了什么问题,会返回JSON格式的错误,而非我们创建的Song对象。
也可以使用变更来改变现有的数据。假设我们想要改变Snowtooth缆车的状态,具体代码如下:
mutation closeLift {
setLiftStatus(id: "jazz-cat", status: CLOSED) {
name
status
}
}
这样我们便把jazz-cat缆车的状态从打开切换为关闭。变更之后,可在选择集中查看最新变更结果,从而得知更改后的缆车名称和状态。
使用查询变量
到目前为止,我们已经通过将新的字符串作为变更参数来修改数据。既然如此,当然也可以输入变量。使用变量来替换查询中的静态值,这样我们便能够按照需求传递参数了。我们来看看addSong变更,把静态字符串换成变量。在GraphQL中,用“$+变量名”来表示变量:
mutation createSong($title:String!, $numberOne:Int, $by:String!) {
addSong(title:$title, numberOne:$numberOne, performerName:$by) {
id
title
numberOne
}
}
静态值被替换成了“$+变量名”。接下来,我们使用参数名来映射每个$变量名称。在GraphiQL或Playground中,有一个查询变量的窗口,会将我们输入的数据作为JSON对象进行发送。请务必使用正确的变量名称作为JSON的键:
{
"title": "No Scrubs",
"numberOne": true,
"by": "TLC"
}
变量在发送参数数据时非常有用,不仅让变更在测试中更有组织性,而且在连接客户端界面时,允许动态输入也会非常方便。
订阅
GraphQL可以提供的第三种操作是订阅(subscription)。有时客户端可能希望从服务器推送实时的更新。订阅功能允许我们监听GraphQL API以进行实时的数据更改。
GraphQL中的订阅功能来自Facebook中的真实用例。开发团队需要一种方法来显示一个贴子在不刷新页面的情况下获得的实时点赞数量。实时点赞是一个由订阅支持的实时用例。每个客户端都订阅了点赞事件,从而能够看到点赞数量的实时变化。
同变更和查询一样,订阅也是根类型。在订阅类型中,客户端可以监听API schema中定义的数据的更改。编写用于监听的GraphQL查询字段也和定义其他操作相类似。
例如,通过订阅功能,我们可以监听任何缆车的状态变化:
subscription {
liftStatusChange {
name
capacity
status
}
}
当开始运行订阅时,我们通过WebSocket监听缆车状态的变更。注意,单击GraphQL Playground中的Play按钮不会立即返回数据。当订阅发送到服务器时,将启动对任何数据更改的监听。
要看看推送到订阅的数据,需要改变一些数据。我们需要打开一个新窗口或选项卡,以便输入变更来触发更改。在GraphQL Playground中运行了订阅操作之后,就不要在同一个窗口或选项卡中进行操作了。如果你使用GraphiQL编写订阅,只需打开第2个浏览器窗口连接到GraphiQL接口即可。
在新窗口或选项卡里,发送更改缆车状态的变更:
mutation closeLift {
setLiftStatus(id: "astra-express", status: HOLD) {
name
status
}
}
当运行这个变更时,Astra Express的状态将会改变,它的name、capacity和status字段将被推送到我们的订阅当中。
让我们再次更改一个缆车的状态。这一次我们试着将Whirlybird缆车的状态设置为关闭。请注意,这份新的信息已经传递到我们的订阅。GraphQL Playground允许你查看两组响应数据以及将数据推送到订阅的时间。
不同于查询和变更,订阅是保持开放的。每次缆车状态发生变化时,新的数据都会推送到该订阅。若要停止监听缆车状态的更改,你需要取消订阅。在GraphQL Playground中按下停止按钮即可。在GraphiQL中则只能关闭订阅所在的浏览器选项卡。
自检
GraphQL最强大的功能之一就是自检(inrospection)。自检是查询当前API schema细节的能力。自检是将GraphQL文档添加到GraphiQL Playground接口的方式。
你可以向每个GraphQL API发送查询,这些查询返回关于API schema的数据。例如,如果我们想了解在Snowtooth中哪些GraphQL类型可用,可以通过运行__schema查询查看这些信息。
query {
__schema {
types {
name
description
}
}
}
当运行这个查询时,可以看到API上所有可用的类型,包括根类型、自定义类型,甚至是标量类型。如果我们想要查看特定类型的详细信息,可以运行__type进行查询,并将想要查询的类型名称作为参数进行发送:
query liftDetails {
__type(name:"Lift") {
name
fields {
name
description
type {
name
}
}
}
}
该自检查询向我们展示了可用于Lift(缆车)类型上查询的所有字段。在遇见新的GraphQL API时,最好通过此方法找出根类型上可用的字段:
query roots {
__schema {
queryType {
...typeFields
}
mutationType {
...typeFields
}
subscriptionType {
...typeFields
}
}
}
fragment typeFields on __Type {
name
fields {
name
}
}
自检查询遵循GraphQL查询语言的规则。前面查询的冗余部分已经通过使用片段进行了优化。我们正在查询类型的名称以及每个根类型下可用的字段。自检功能可以帮助客户端了解当前API schema的工作方式。
抽象语法树
抽象语法树(Abstract Syntax Tree,AST)是一种表示查询的分层结构对象,它包含了GraphQL查询详细信息的嵌套字段。查询文档是一个字符串。当我们向GraphQL API发送查询时,该字符串将会被解析为抽象语法树,并在操作之前进行验证。
这个过程的第一步是将字符串解析为一组更小的片段,具体来说就是将关键字、参数甚至方括号和冒号解析为一组单独的标记。这个过程被称为词法分析(lexical analysis)。接下来,将词法分析解析成抽象语法树。AST形态的查询字段更容易修改和验证。
例如,你的查询从GraphQL文档开始。文档至少包含一个定义(definition),但也可包含一个定义列表。定义只能是OperationDefiniation或FragmentDefinition中的一种。下面是一个包含了三个定义的文档示例:两个操作和一个片段。
query jazzCatStatus {
Lift(id: "jazz-cat") {
name
night
elevationGain
trailAccess {
name
difficulty
}
}
}
mutation closeLift($lift: ID!) {
setLiftStatus(id: $lift, status: CLOSED ) {
...liftStatus
}
}
fragment liftStatus on Lift {
name
status
}
一个OperationDefinition只能包含三种操作类型(mutation、query、subscription)中的一种。每个操作定义都包含了OperationType(操作类型)和SelectionSet(选择集)。
每个操作后面的大括号里面就是本次操作的选择集。这些是我们要查询的实际字段和参数。例如,Lift字段是jazzCatStatus查询的选择集,而setLiftStatus字段表示closelift变更的选择集。
选择集之间是可以嵌套的。jazzCatStatus查询包含3个嵌套选择集。第一个选择集包含Lift字段,接着嵌套了一个包含name、night、elevationGain和trailAccess字段的选择集,再接着又嵌套了另一个包含name和difficulty字段的选择集。
GraphQL可以遍历这个AST并根据GraphQL语言和当前schema对其详细信息进行验证。如果查询语言的语法正确并包含了我们请求的字段和类型,则执行操作;否则将返回一个特定错误。
此外,这个AST对象比字符串更容易修改。如果我们想在jazzCatStatus查询中追加打开的缆车数量,可以直接修改AST,仅需在操作中添加一个额外的选择集即可。AST是GraphQL的重要部分。每个操作都被解析为AST,以便验证并最终执行。
在本章中,你了解了GraphQL查询语言。现在我们可以使用这种语言来和GraphQL服务交互了。但是,如果没有特定GraphQL服务可用的操作和字段的具体定义,这一切都不可能实现。这个特定的定义就是我们之前曾数次提到过的GraphQL schema,下一章就是它的主场。