GraphQL 是一个Facebook于2012开发出来且2015开源的应用层的查询语言,你需要在后台定义一个基于GraphQL的图形模式(schema),然后你的客户端就可以查询他们想要的数据,而不需要后台重新定义一个接口返回你需要的数据.
因为不需要更改你后台,所以这种方式比 REST API
方式更好,让我们可以在不同的客户端上灵活改变数据显示.
# DOS命令下,管理员身份运行:
$ mkdir graphql-intro && cd ./graphql-intro
$ npm init
$ npm install express --save
$ npm install graphql --save
$ npm install express-graphql --save
$ npm install babel-core --save
$ npm install babel-preset-es2015 --save
$ touch ./server.js
$ touch ./index.js
$ npm install -d
# 加环境变量:C:\Windows\System32\
# 重启电脑
$ npm install nodemon -g
GraphQL 由以下组件构成:
类型系统(Type System)
查询语言(Query Language)
执行语义(Execution Semantics)
静态验证(Static Validation)
类型检查(Type Introspection)
作为将数据模型和具体接口实现解耦的 DSL,GraphQL 的基础组件,也是它最重要的组件之一就是类型系统。
可以将 GraphQL 的类型系统分为标量类型(Scalar Types,标量类型)和其他高级数据类型,标量类型即可以表示最细粒度数据结构的数据类型,可以和 JavaScript 的原始类型对应。
3.1.1 标量类型
Int :整数,对应 JavaScript 的 Number
Float :浮点数,对应 JavaScript 的 Number
String :字符串,对应 JavaScript 的 String
Boolean :布尔值,对应 JavaScript 的 Boolean
ID :ID 值,是一个序列化后值唯一的字符串,可以视作对应 ES 2015 新增的 Symbol
3.1.2 高级数据类型
3.1.2.1 Object :对象
用于描述层级或者树形数据结构。对于树形数据结构来说,叶子字段的类型都是标量数据类型。几乎所有 GraphQL 类型都是对象类型。Object 类型有一个 name 字段,以及一个很重要的 fields 字段。fields 字段可以描述出一个完整的数据结构。
例如一个表示地址数据结构的 GraphQL 对象为:
const AddressType = new GraphQLObjectType({
name: 'Address',
fields: {
street: { type: GraphQLString },
number: { type: GraphQLInt },
formatted: {
type: GraphQLString,
resolve(obj) {
return obj.number + ' ' + obj.street
}
}
}
});
3.1.2.2 Interface :接口
接口用于描述多个类型的通用字段,例如一个表示实体数据结构的 GraphQL 接口为:
const EntityType = new GraphQLInterfaceType({
name: 'Entity',
fields: {
name: { type: GraphQLString }
}
});
3.1.2.3 Union :联合
联合类型用于描述某个字段能够支持的所有返回类型以及具体请求真正的返回类型,例如一个表示宠物(可以是猫或者狗)的 GraphQL 联合类型为:
const PetType = new GraphQLUnionType({
name: 'Pet',
types: [DogType, CatType],
resolveType(value) {
if (value instanceof Dog) {
return DogType;
}
if (value instanceof Cat) {
return CatType;
}
}
});
3.1.2.4 Enum :枚举
用于表示可枚举数据结构的类型,例如表示 RGB 色值的 GraphQL 枚举类型为:
const RGBType = new GraphQLEnumType({
name: 'RGB',
values: {
RED: { value: 0 },
GREEN: { value: 1 },
BLUE: { value: 2 },
}
});
3.1.2.5 Input Object :输入对象
是为了查询(query)而定义的数据类型,不直接重用 Object 类型是因为 Object 的字段可能存在循环引用,或者字段引用了不能作为查询输入对象的接口和联合类型。
参考实现中 Input Object 的定义代码为:
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
);
}
可以看到,Object、Interface 和 Union 三种类型是不能作为输入对象类型的。
3.1.2.6 List :列表
列表是其他类型的封装,通常用于对象字段的描述。例如下面 PersonType 类型数据的 parents 和 children 字段:
const PersonType = new GraphQLObjectType({
name: 'Person',
fields: () => ({
parents: { type: new GraphQLList(Person) },
children: { type: new GraphQLList(Person) },
})
});
3.1.2.7 Non-Null :不能为 Null
Non-Null 强制类型的值不能为 null,并且在请求出错时一定会报错。可以用于必须保证值不能为 null 的字段。例如数据库的行的 id 字段不能为 null:
const RowType = new GraphQLObjectType({
name: 'Row',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLString) }
})
});
还有一种重要的数据类型,即 schema 类型,它描述了后端服务器能够提供的数据支持。这里先暂时不介绍,因为它涉及 GraphQL 的其他组件,等全部介绍完我们再来看 GraphQL 中 schema 的 具体实现 。
类型系统对应我们开头提到的 Schema,是对服务器端数据的描述,而查询语言则解耦了前端开发者与后端接口的依赖。前端开发者利用查询语言可以自由地组织和定制系统能够提供的业务数据。
GraphQL 的一个查询请求被称为一份 query 文档(query document),即 GraphQL 服务能够解析验证并执行的一串请求字符串。query 由操作(Operation)和片段(Fragments)组成。一个 query 可以包含多个操作和片段。只有包含操作的 query 才会被 GraphQL 服务执行。但是不包含操作,只有片段的 query 也会被 GraphQL 服务解析验证,这样一份片段就可以在多个 query 文档内使用。
只包含一个操作的 query 可以不带操作名称或者使用简写形式(即 query 关键字加操作名)。query 包含多个操作时,所有操作都必须带上名称。
3.2.1 操作(Operations)
GraphQL 规范支持两种操作
query:仅获取数据(fetch)的只读请求(R)
mutation:获取数据后还有写操作的请求(CUD)
在官方提供的参考实现中我们会发现还支持一种操作 subscription ,这是为了处理订阅更新这种比较复杂的实时数据更新场景而设计的操作,不过目前这种操作还处于试验阶段,不建议在生产环境中使用。
查询请求的模型可以用下面的图来表示
3.2.2 选择集合(Selection Sets)
选择集合表示当前选中的数据内容,格式为:
{
Field // 字段名
FragmentSpread // 片段展开
InlineFragment // 内联片段
}
关于选择集合的使用,可以参考 graphql-js 的代码 。参考实现代码在 这里 。
3.2.3 字段(Field)
字段格式为:
alias:name(argument:value) `alias` 是字段的别名,即结果中显示的字段名称。
name
为字段名称,对应 schema 中定义的 fields 字段名。
argument
为参数名称,对应 schema 中定义的 fields 字段的参数名称。
value
为参数值,值的类型对应标量类型的值。
例如这样的请求:
http://yunhe.taobao.com/?query={banner{backgroundURL:bg,biaoti:slogan}}
backgroundURL 就是 bg 字段的别名。
3.2.4 片段(Fragment)
片段是 GraphQL 的主要组合数据结构,通过片段可以重用重复的字段选择,减少 query 中的重复内容。片段又分为 FragmentSpread 和 InlineFragment。
例如没有片段时需要这样编写 query:
query noFragments {
user(id: 4) {
friends(first: 10) {
id
name
profilePic(size: 50)
}
mutualFriends(first: 10) {
id
name
profilePic(size: 50)
}
}
}
query 中存在下列重复的选择集合:
{
id
name
profilePic(size: 50)
}
可以用片段简化为:
query withFragments {
user(id: 4) {
friends(first: 10) {
...friendFields
}
mutualFriends(first: 10) {
...friendFields
}
}
}
fragment friendFields on User {
id
name
profilePic(size: 50)
}
使用片段时需要加上 ...
操作符表示展开片段内容。内联片段示例如下:
query inlineFragmentTyping {
profiles(handles: ["zuck", "cocacola"]) {
handle
... on User {
friends {
count
}
}
... on Page {
likers {
count
}
}
}
}
3.2.5 指令(Directives)
指令要解决的是 query 执行时字段参数无法覆盖的情况,例如引入或者忽略某个字段。指令为 GraphQL 执行添加了更多的信息。
指令实例如下:
query hasConditionalFragment($condition: Boolean) {
...maybeFragment @include(if: $condition)
}
fragment maybeFragment on Query {
me {
name
}
}
include 指令表示只有在 if 参数为 true 时才引入片段表示的字段。
skip 指令表示在 if 参数为 true 时忽略片段中的字段。
熟悉了 类型系统 和 查询语言 我们就可以用 GraphQL 来实现应用层的数据请求了。其他三个 GraphQL 组件更偏向于 DSL 的实现和原理,因此本文不再做详细介绍,感兴趣的同学可以对照 规范 和 参考实现 自己研究。
$ git clone https://github.com/zhouyuexie/learn-graphql
$ cd learn-graphql && npm install
$ npm start
现在打开你的浏览器输入http://localhost:12580/graphql
,或者点击这里.
虽然GraphQL看起来很酷炫,但是也有些地方要注意。
1、 服务端优化
由于是查询本身被解析成图,递归地取值,因此可能会存在服务器性能隐患。特别是对SQL来说,现在非常容易大量出现N+1的情形。因此,现在GraphQL大多还都用在NoSQL上。但是由于整个请求还是在一次HTTP请求中完成的,理论上我们也有Batch为一个查询的能力,就像许多ORM有一些惰性特性,可以将多个查询过滤语句合并成一条查询一样。Facebook正在研究如何让GraphQL更好地Batch问题比如dataloader,社区也有一些不错的实现。这些库不仅在尝试解决这些问题,而且也揭示了GraphQL作为API抽象层本身,可以在普通Web场景下和ORM结合的能力——通过代码可以将这两层抽象到一起,这很可能让我们可以自己轻易搭建一个GraphQL BAAS(Back-end as a service)。现在已经有很多平台提供这种服务了,比如scaphold、reindex、graphcool等,也有Graffiti这种与Mongo ODM直接结合的类库。
2、安全问题
虽然GraphQL给客户端提供了强大的查询能力,但这也意味着有被客户端滥用的风险。如果不使用某些限制过大的查询,反复请求一条Load出所有Github用户的查询可能会让他们的服务器直接挂掉,GraphQL提高了被DDoS的风险。因此在使用GraphQL的过程中,我们要对安全问题更加重视。
3、需要重新思考Cache策略
REST虽然会引起一些性能问题,但它也以HTTP Cache的方式解决了很多性能问题。而对于多变的GraphQL操作来说,Cache就变成一个需要深入讨论的话题了。然而这种Cache策略就要交给客户端来完成了。
4、然后呢?
因此,接下来的文章会深入讨论基于GraphQL的多种类库以及不同客户端。最终我们也可以在这些类库上看到在现代组件化趋于主流之后,我们的通信应该怎么与组件化设计有效结合。与此同时,随着客户端越来越复杂,我们应该如何同步服务端状态,如何管理缓存等等。
其中一个是Meteor团队推出的Apollo Data,它提供了一系列的服务端以及客户端工具来简化GraphQL的开发,容易上手并且支持Android、IOS、React(Native)以及Angular2。而另外一个更复杂但更强大的选择则是Facebook自己推出的Relay,它支持React(Native),提供了类似Virtual DOM的Diff算法来Diff Cache,可以自动精确管理数据来解决Cache问题。
我们将会从Apollo开始,感受GraphQL究竟是如何工作的,之后我们也会看到这个方案中的一些问题,而这些问题会将我们引向更深层的Web客户端问题——最终我们会探讨Relay,一个更完整的解决方案,从中我们可以看到Facebook对Web客户端的未来有着怎样的思考。