在前一篇文章中我们对GraphQL有了基础的了解,我们直到GraphQL使用Schema
来描述数据,并通过指定和实现GraphQL规范定义了支持Schema
查询的DSQL (Domain Specific Query Language,领域特定查询语言)
。Schema
帮助将复杂的业务模型数据抽象拆分成细粒度的基础数据结构,而DSQL
的实现则赋予了前端开发者自由组织和定制请求数据的能力。
如果以一张图来表示的话,可以将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 | const AddressType = new GraphQLObjectType({ |
2. 接口(Interface)
接口用于描述多个类型的通用字段,例如一个表示实体数据结构的GraphQL接口为
1 | const EntityType = new GraphQLInterfaceType({ |
3. 联合(Union)
联合类型用于描述某个字段能够支持的所有返回类型以及具体请求真正的返回类型,例如一个表示宠物(可以是猫或者狗)的GraphQL联合类型为
1 | const PetType = new GraphQLUnionType({ |
4. 枚举(Enum)
用于表示可枚举数据结构的类型,例如表示RGB色值的GraphQL枚举类型为
1 | const RGBType = new GraphQLEnumType({ |
5. 输入对象(Input Object)
是为了查询(query
)而定义的数据类型,不直接重用Object
类型是因为Object
的字段可能存在循环引用,或者字段引用了不能作为查询输入对象的接口和联合类型。参考实现中的Input Object
的定义代码为
1 | export type GraphQLInputType = |
可以看到,Object
、Interface
和Union
三种类型是不能作为输入对象类型的。
6. 列表(List)
列表是其它类型的封装,通常用于对象字段的描述。例如下面PersonType
类型数据的parents
和children
字段
1 | const PersonType = new GraphQLObjectType({ |
7. 不能为Null(Non-Null)
Non-Null
强制类型的值不能为null
,并且在请求出错时一定会报错。可以用于必须保证值不能为null
的字段。例如数据库的id字段不能为null
1 | const RowType = new GraphQLObjectType({ |
还有一种重要的数据类型,即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 | { |
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
中的重复内容。片段又分为FragmentSpread
和InlineFragment
。例如没有片段时需要这样编写query
1 | query noFragments { |
query
中存在下列重复的选择集合
1 | { |
可以用片段化简为
1 | query withFragments { |
使用片段时需要加上...
操作符表示展开片段内容。
内联片段示例如下
1 | query inlineFragmentTyping { |
5. 指令(Directives)
指令要解决的是query
执行时字段参数无法覆盖的情况,例如引入或者忽略某个字段。指令为GraphQL执行添加了更多的信息。
指令实例如下
1 | query hasConditionalFragment($condition: Boolean) { |
include
指令表示只有在if
参数为true时才引入片段表示的字段。
skip
指令表示在if
参数为true时忽略片段中的字段。
熟悉了类型系统和查询语言,我们就可以用GraphQL来实现应用层的数据请求了。
其他三个GraphQL组件更偏向于DSL的实现和原理,本文不再做详细介绍。