张啸


世界上最快乐的事,莫过于为理想而奋斗。


Web(3) GraphQL深入理解

在前一篇文章中我们对GraphQL有了基础的了解,我们直到GraphQL使用Schema来描述数据,并通过指定和实现GraphQL规范定义了支持Schema查询的DSQL (Domain Specific Query Language,领域特定查询语言)Schema帮助将复杂的业务模型数据抽象拆分成细粒度的基础数据结构,而DSQL的实现则赋予了前端开发者自由组织和定制请求数据的能力。

如果以一张图来表示的话,可以将GraphQL看做一条以通用基础业务数据模型为基础、将传统后端服务和前端页面紧密且自由地联系在一起的纽带。

GraphQL

为什么GraphQL的Schema能够表示出服务器所支持的复杂业务模型数据,GraphQL的Query又是怎样赋予前端开发者对数据的定制能力,本文将通过分析和理解GraphQL的设计来解答这些问题。


一、GraphQL的设计

GraphQL由以下组件构成

  • 类型系统(Type System

  • 查询语言(Query Language

  • 执行语义(Execution Semantics

  • 静态验证(Static Validation

  • 类型检查(Type Introspection

作为将数据模型和具体接口实现解耦的DSL,GraphQL的基础组件,也是它最重要的组件之一就是类型系统。


二、类型系统

可以将GraphQL的类型系统分为标量类型(Scalar Types,标量类型)和其他高级数据类型,标量类型即可以表示最细粒度数据结构的数据类型,可以和JavaScript的原始类型对应。

GraphQL规范目前规定支持的标量类型有

  • Int:整数,对应JavaScript的Number

  • Float:浮点数,对应JavaScript的Number

  • String:字符串,对应JavaScript的String

  • Boolean:布尔值,对应JavaScript的Boolean

  • ID:ID值,是一个序列化后值唯一的字符串,可以视作对应ES6新增的Symbol

其他高级数据类型包括

1. 对象(Object)

用于描述层级或者树形数据结构。对于树形数据结构来说,叶子字段的类型都是标量数据类型。几乎所有的GraphQL类型都是对象类型。Object类型有一个name字段,以及一个很重要的fields字段。fields字段可以描述出一个完整的数据结构。例如一个表示地址数据机构的GraphQL对象为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const AddressType = new GraphQLObjectType({
name: 'Address',
fields: {
street: {
type: GraphQLString
},
number: {
type: GraphQLInt
},
formatted: {
type: GraphQLString,
resolve(obj) {
return obj.name + ' ' + obj.street;
}
}
}
})

2. 接口(Interface)

接口用于描述多个类型的通用字段,例如一个表示实体数据结构的GraphQL接口为

1
2
3
4
5
6
7
8
const EntityType = new GraphQLInterfaceType({
name: 'Entity',
fields: {
name: {
type: GraphQLString
}
}
});

3. 联合(Union)

联合类型用于描述某个字段能够支持的所有返回类型以及具体请求真正的返回类型,例如一个表示宠物(可以是猫或者狗)的GraphQL联合类型为

1
2
3
4
5
6
7
8
9
10
11
12
const PetType = new GraphQLUnionType({
name: 'Pet',
types: [DogType, CatType],
resolveType(value) {
if (value instanceof Dog) {
return DogType;
}
if (value instanceof Cat) {
return CatType;
}
}
});

4. 枚举(Enum)

用于表示可枚举数据结构的类型,例如表示RGB色值的GraphQL枚举类型为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const RGBType = new GraphQLEnumType({
name: 'RGB',
values: {
RED: {
value: 0
},
GREEN: {
value: 1
},
BLUE: {
value: 2
}
}
});

5. 输入对象(Input Object)

是为了查询(query)而定义的数据类型,不直接重用Object类型是因为Object的字段可能存在循环引用,或者字段引用了不能作为查询输入对象的接口和联合类型。参考实现中的Input Object的定义代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export type GraphQLInputType = 
GraphQLScalarType |
GraphQLEnumType |
GraphQLInputObjectType |
GraphQLList<GraphQLInputType> |
GraphQLNonNull<
GraphQLScalarType |
GraphQLEnumType |
GraphQLInputObjectType |
GraphQLList<GraphQLInputType>
>;

export function isInputType(type: ?GraphQLType): boolean {
const namedType = getNamedType(type);
return {
namedType instanceof GraphQLScalarType ||
namedType instanceof GraphQLEnumType ||
namedType instanceof GraphQLInputObjectType
};
}

可以看到,ObjectInterfaceUnion三种类型是不能作为输入对象类型的。

6. 列表(List)

列表是其它类型的封装,通常用于对象字段的描述。例如下面PersonType类型数据的parentschildren字段

1
2
3
4
5
6
7
8
9
10
11
const PersonType = new GraphQLObjectType({
name: 'Person',
fields: () => ({
parents: {
type: new GraphQLList(Person)
},
children: {
type: new GraphQLList(Person)
}
})
});

7. 不能为Null(Non-Null)

Non-Null强制类型的值不能为null,并且在请求出错时一定会报错。可以用于必须保证值不能为null的字段。例如数据库的id字段不能为null

1
2
3
4
5
6
7
8
const RowType = new GraphQLObjectType({
name: 'Row',
fields: () => ({
id: {
type: new GraphQLNonNull(GraphQLString)
}
})
})

还有一种重要的数据类型,即schema类型,它描述了后端服务器能够提供的数据支持。


三、查询语言

类型系统对应我们开头提到的Schema,是对服务器端数据的描述,而查询语言则解耦了前端开发者与后端接口的依赖。前端开发者利用查询语言可以自由地组织和定制系统能够提供的业务数据。

GraphQL的一个查询请求被称为一份query文档(query document),即GraphQL服务能够解析验证并执行的一串请求字符串。query由操作(Operation)和片段(Fragments)组成。一个query可以包含多个操作和片段。只有包含操作的query才会被GraphQL服务执行。但是不包含操作,只包含query也会被GraphQL服务解析验证,这样一份片段就可以在多个query文档内使用。

只包含一个操作的query可以不带操作名称或者使用简写形式(即query关键字加名)。query包含多个操作时,所有操作都必须带上名称。

1. 操作(Operation)

GraphQL规范支持两种操作

  • query:仅获取数据(fetch)的只读请求

  • mutation:获取数据后还有写操作的请求

在官方提供的参考实现中我们会发现还有一种操作subscription,这是为了处理订阅更新这种比较复杂的实时数据更新场景而设计的操作,不过目前这种操作还处于试验阶段,不建议在生产环境使用。

查询请求的模型可以用下面的图来表示

查询请求模型

2. 选择集合(Selection Sets)

选择集合表示当前选中的数据内容,格式为

1
2
3
4
5
{
Field // 字段名
FragmentSpread // 片段展开
InlineFragment // 内联片段
}

3. 字段(Field)

字段格式为

1
alias:name(argument:value)

其中alias是字段的别名,即结果中显示的字段名称。

name为字段名称,对应schema中定义的fields字段名。

argument为参数名称,对应schema中定义的fields字段的参数名称。

value为参数值,值的类型对应标量类型的值。

例如这样的请求:http://example.taobao.com/?query={banner{backgroundURL:bg,biaoti:slogan}},其中backgroundURL就是bg字段的别名。

4. 片段(Fragment)

片段时GraphQL的主要组合数据结构,通过片段可以重用重复的字段选择,减少query中的重复内容。片段又分为FragmentSpreadInlineFragment。例如没有片段时需要这样编写query

1
2
3
4
5
6
7
8
9
10
11
12
13
14
query noFragments {
user(id: 4) {
friends(first: 10) {
id
name
profilePic(size: 50)
}
mutualFriends(first: 10) {
id
name
profilePic(size: 50)
}
}
}

query中存在下列重复的选择集合

1
2
3
4
5
{
id
name
profilePic(size: 50)
}

可以用片段化简为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
query withFragments {
user(id: 4) {
friends(first: 10) {
...friendFields
}
mutualFriends(first: 10) {
...friendFields
}
}
}

fragment friendFields on User {
id
name
profilePic(size: 50)
}

使用片段时需要加上...操作符表示展开片段内容。

内联片段示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
query inlineFragmentTyping {
profiles(handles: ["zuck", "cocacola"]) {
handle
... on User {
friends {
count
}
}
... on Page {
likers {
count
}
}
}
}

5. 指令(Directives)

指令要解决的是query执行时字段参数无法覆盖的情况,例如引入或者忽略某个字段。指令为GraphQL执行添加了更多的信息。

指令实例如下

1
2
3
4
5
6
7
8
9
query hasConditionalFragment($condition: Boolean) {
...maybeFragment @include(if: $condition)
}

fragment maybeFragment on Query {
me {
name
}
}

include指令表示只有在if参数为true时才引入片段表示的字段。

skip指令表示在if参数为true时忽略片段中的字段。

熟悉了类型系统查询语言,我们就可以用GraphQL来实现应用层的数据请求了。

其他三个GraphQL组件更偏向于DSL的实现和原理,本文不再做详细介绍。


参考文献

  1. GraphQL | 一种为你的API而生的查询语言

  2. 深入理解GraphQL