GraphQL 基础

GraphQL是一个API查询语言,它能够对现有API中的数据进行完整的和可理解的描述。
使客户端一次查询就能够准确的获取到想要的所有字段,不多也不少。
这样随着时间的推移,使得API开发升级更容易。 GraphQL也提供了强大的开发工具,协助我们在开发过程中更方便的写查询语句。
官网入口

Queries and Mutations 查询和修改

Feilds 字段

GraphQL可以在查询中列出需要的字段,要求服务端返回这些字段:

1
2
3
4
5
{
hero {
name
}
}

如上面的查询,会获得如下返回:

1
2
3
4
5
6
7
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}

查询语句里可以写注释:

1
2
3
4
5
6
7
8
9
{
hero {
name
# Queries can have comments!
friends {
name
}
}
}

Arguments 参数

GraphQL查询支持传参数:

1
2
3
4
5
6
{
human(id: "1000") {
name
height
}
}

Alias 别名

查询语句支持给字段取别名,这样做就可以根据不同的参数,获取分别同一个字段:

1
2
3
4
5
6
7
8
{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}

Fragments 片段

代码片段, 可以把一些公共的查询提取出来, 取个名字, 到处复用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}

fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}

引用代码片段: ...fragmentName

Operation Name 操作名

操作可以取名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
query HeroComparison($first: Int = 3) {
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}

fragment comparisonFields on Character {
name
friendsConnection(first: $first) {
totalCount
edges {
node {
name
}
}
}
}

推荐给操作取名, 方便定位问题,打印日志

Operation Types 操作类型

query, mutation, subscription, describes
简写语法 可以省略 operation type。 默认是 query, 这种情况就不能定义 operation name 和参数了。

Variables 变量

变量 解决的是 “手工String插值” 的问题
在 Operation Name’s Arguments里定义变量, ($varName: Types). 变量名以 $ 符号开头, 冒号后跟变量类型
然后通过单独的变量字典(通常是json)传递变量值: {“varName”: “1111”}。
变量可以定义为 可选的 或者 必须的(在变量类型后跟 ! 表示 必须的),如果如果接收变量的字段需要非空参数, 那变量隐式为必须的。
默认值: 定义变量的时候可以设置默认值, 通过 = 号

Directives 指令

指令(结合变量) 解决的是 变量 查询结构的问题
指令 以 @ 符号开头, 跟在字段或者片断 后面。
目前 GraphQL 规范明确包含两个指令: 所有的 GraphQL 兼容服务端都必须支持:

  1. @include(if: Boolean) 参数为 true 的时候包含此字段
  2. @skip(if: Boolean) 参数为 true 的时候跳过此字段

Mutations 修改

通过 mutation operation type 来执行修改, 修改也会返回修改里的字段
多个修改是串行的, 多个查询是并行的

Inline Fragments 内嵌片段

主要是解决字段是接口或者联合 类型时, 根据值的实际类型,获取不同信息

Meta fields 元字段

内嵌片段判断实际类型时,如果不知道字段具体有哪些实际类型, 可以通过 __typename 元字段 来获取每个值的实际类型。
GranphQL定义了一些元字段, https://graphql.org/learn/introspection/

Schemas and Types 结构和类型

Type System

The exact description of the data we can ask for.
我们能请求的数据 的准确描述,比如:可能选择的字段,返回的是哪种对象,这些子对象有哪些可用字段等。 这些就是 schema 解决的问题。
GraphQL 服务端 都会定义它能查询到的所有类型, 以及关于这些类型的完整描述。当查询进来的时候, 服务端会根据 schema 来校验和执行查询。

Type language

GraphQL schema language

Object types and fields

Object 类型是组成 schema 的最基本组件, 它会包含一些字段。 定义:

1
2
3
4
type Character {
name: String!
appearsIn: [Episode!]!
}
  1. type 关键字定义了一个叫 Character 的对象类型, 意味着这个类型会拥有一些字段, schema中的大多数类型,都会是对象类型。
  2. nameappearsInCharacter 对象类型的字段,意思是在GraphQL query 语句里, 这两个字段只能出现在 操作Character类型时。
  3. String 是内置的 scalar types,scalar types 就不能再有子字段了。
  4. String! 表示此字段是 non-null 的, 服务端返回的这个字段一定有值
  5. [Episode!]! 表示 这是一个 Episode对象数组。外层的! 表示此这数组是 non-null的(可以是空数组), 里面的! 表示数组的每个元素都是 non-null 的

Arguments 参数

GraphQL 对象类型的所有字段, 都可以有0个或多个参数, 比如下面的 length字段:

1
2
3
4
5
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}

所有参数都要命名,传参是根据名字, 不按顺序。 参数可以是可选的, 也可以是必须的; 可选参数可以有默认值。比如上例中 unit参数的默认值是METER

The Query and Mutation types

schema 中多数类型都是常规的对象类型,但是有两个特殊的:

1
2
3
4
schema {
query: Query
mutation: Mutation
}

每个 GraphQL 服务都有query类型,可能有mutation类型, 他们和普通对象类型一样的, 唯一特殊的一点是他们定义了每个GraphQL查询的入口。

对于 query 查询中用到的所有字段, 在服务端的 Query类型定义里都必须要出现,
比如下面这个query:

1
2
3
4
5
6
7
8
query {
hero {
name
}
droid(id: "2000") {
name
}
}

那在服务端Query类型的定义里,就至少需要有如下字段:

1
2
3
4
type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}

Scalar types 标量类型

标量就不能再有子字段了,是不可再的拆分的类型, 是查询的叶子节点。GraphQL的内置标量有:

  1. Int: 有符号32位整型
  2. Float: 有符号双精度浮点数
  3. String: UTF-8 字符串
  4. Booleantrue or false
  5. ID:表示唯一标识, 通常用来获取对象, 或者做为缓存的key,ID类型通过序列化为String, 定义为ID类型表示人类不可读。

多数GraphQL 服务实现里, 都支持处定义标量, 比如定义一个名叫Date 的标量类型:

1
scalar Date

然后由GraphQL 服务实现 来定义 这个类型的序列化、反序列化和验证。

Enumeration types 枚举

枚举是特殊的标量, 只允许它定义的几个值。比如定义一个叫Episode 的枚举:

1
2
3
4
5
enum Episode {
NEWHOPE
EMPIRE
JEDI
}

Lists and Non-Null 集合和 Non-null

没有什么特殊的, 就是传统意义上的集合和 Non-null。

Interfaces 接口

接口是抽象类型,实现它的子类型, 必须包含接口中定义的字段。 定义一个Character接口:

1
2
3
4
5
6
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}

再定义两个子类型HumanDroid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}


type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}

Union types 联合类型

联合类型与接口非常相似,但是它不能指定类型之间的任何公共字段。

1
union SearchResult = Human | Droid | Starship

注意,联合类型的成员必须是具体的对象类型;不能是接口或其他联合类型。
查询的时候通过需要使用 Inline Fragment来处理接口和联合类型。

Input types 输入类型

输入类型在修改时特别有用,可以用来把整个对象做为参数传递。定义input type:

1
2
3
4
input ReviewInput {
stars: Int!
commentary: String
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
#values
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}

输入对象类型上的字段本身可以引用输入对象类型,但在schema中不能混合输入和输出类型,输入对象类型的字段
也不能有参数。