---
GraphQL [ɡræf]
---
### GraphQL是什么? (graph n:图; 图表; 曲线图) 美:[ɡræf]
>关于GraphQL是什么,网上一搜一大堆。根据官网的解释就是一种用于 API 的查询语言。
>一看到用于API的查询语言,我也是一脸懵逼的。博主你在开玩笑吧?你的翻译水平不过关?API还能查吗?API不是后端写好,前端调用的吗?
的确可以,这就是GraphQL强大的地方。
### 为什么要使用GraphQL?
>在实际工作中往往会有这种情景出现:比如说我需要展示一个商品的列表,可接口却会把商品的描述,更新时间,创建者等各种各样的 (无用的) 信息都一同返回。
问了后端,原因大概如下:
>* 原来是为了兼容PC端和移动端用同一套接口
>* 或者在整个页面,这里需要显示商品的标题,可是别的地方需要显示商品明细啊,避免多次请求我就全部返回咯
>* 或者是因为有时候项目经理想要显示“标题+更新时间”,有时候想要点击标题展开商品明细等等需求,所以把商品相关的信息都一同返回
简单说就是:
>* 兼容多平台导致字段冗余
>* 一个页面需要多次调用 API 聚合数据
>* 需求经常改动导致接口很难为单一接口精简逻辑
有同学可能会说那也不一定要用GraphQL啊,比方说第一个问题,不同平台不同接口不就好了嘛
```
http://api.xxx.com/web/getGameInfo/:gameID
http://api.xxx.com/app/getGameInfo/:gameID
http://api.xxx.com/mobile/getGameInfo/:gameID
```
或者加个参数也行
```
http://api.xxx.com/getGameInfo/:gameID?platfrom=web
```
这样处理的确可以解决问题,但是无疑加大了后端的处理逻辑。你真的不怕后端程序员打你?
这个时候我们会想,接口能不能不写死,把静态变成动态?
### GraphQL尝尝鲜——(GraphQL简单例子)
下面是用GraphQL.js和express-graphql搭建一个的普通GraphQL查询(query)的例子,包括讲解GraphQL的部分类型和参数,已经掌握了的同学可以跳过。
> 1.先跑个hello world
>* 1.新建一个graphql文件夹,然后在该目录下打开终端,执行npm init --y初始化一个packjson文件。
>* 2.安装依赖包:npm install --save -D express express-graphql graphql
>* 3.新建schema.js文件,填上下面的代码
>1.hello world
>* 先简单讲解一下代码:
```typescript
const queryObj = new GraphQLObjectType({
name: 'myFirstQuery',
description: 'a hello world demo',
fields: {}
});
```
GraphQLObjectType是GraphQL.js定义的对象类型,包括name、description 和fields三个属性,其中name和description 是非必填的。fields是解析函数,在这里可以理解为查询方法
```typescript
hello: {
name: 'a hello world query',
description: 'a hello world demo',
type: GraphQLString,
args: {
name: { // 这里定义参数,包括参数类型和默认值
type: GraphQLString,
defaultValue: 'Brian'
}
},
resolve(parentValue, args, request) { // 这里演示如何获取参数,以及处理
return 'hello world ' + args.name + '!';
}
}
```
对于每个fields,又有name,description,type,resolve参数,这里的type可以理解为hello方法返回的数据类型,resolve就是具体的处理方法。
说到这里有些同学可能还不满足,如果我想每次查询都想带上一个参数该怎么办,如果我想查询结果有多条数据又怎么处理?
schema.js文件,来一个加强版的查询(当然,你可以整理一下代码,我这样写是为了方便阅读)
```typescript
const {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLBoolean
} = require('graphql');
const queryObj = new GraphQLObjectType({
name: 'myFirstQuery',
description: 'a hello world demo',
fields: {
hello: {
name: 'a hello world query',
description: 'a hello world demo',
type: GraphQLString,
args: {
name: { // 这里定义参数,包括参数类型和默认值
type: GraphQLString,
defaultValue: 'Brian'
}
},
resolve(parentValue, args, request) { // 这里演示如何获取参数,以及处理
return 'hello world ' + args.name + '!';
}
},
person: {
name: 'personQuery',
description: 'query a person',
type: new GraphQLObjectType({ // 这里定义查询结果包含name,age,sex三个字段,并且都是不同的类型。
name: 'person',
fields: {
name: {
type: GraphQLString
},
age: {
type: GraphQLInt
},
sex: {
type: GraphQLBoolean
}
}
}),
args: {
name: {
type: GraphQLString,
defaultValue: 'Charming'
}
},
resolve(parentValue, args, request) {
return {
name: args.name,
age: args.name.length,
sex: Math.random() > 0.5
};
}
}
}
});
module.exports = new GraphQLSchema({
query: queryObj
});
```
打开http://localhost:8000,在左侧输入
```typescript
{
hello(name:"charming"),
person(name:"charming"){
name,
sex,
age
}
}
```
右侧就会显示出:
![示意图](./images/20180904175443716.png)
你可以在左侧仅输入person方法的sex和age两个字段,这样就会只返回sex和age的信息。动手试一试吧!
```typescript
{
person(name:"charming"){
sex,
age
}
}
```
当然,结果的顺序也是按照你输入的顺序排序的。
定制化的数据,完全根据你查什么返回什么结果。这就是GraphQL被称作API查询语言的原因。
> GraphQL是应用层查询语言,它有自己的数据类型,用来描述数据,比如标量类型、集合类型、Object类型、枚举等,甚至有接口类型。而GraphQL服务端不依赖任何语言,我只是从概念上来了解其数据类型。
### 标量类型(Scala)
> 标量类型是不能包含子字段,主要有如下类型:
> * Int: 有符号的32位整型
> * Float: 有符号的双精度浮点型
> * String: UTF‐8字符序列
> * Boolean: 布尔型
> * ID: 常用于获取数据的唯一标志,或缓存的键值,它也会被序列化为String,但可读性差。
> 以上的类型是GraphQL规范的基本类型,每个规范的实现也可以有自定以标量,如 scala Date ,但它必须能够>序列和反序列化,至于如何定于取决于具体的规范实现。
1、枚举(Enum)
枚举类型是标量类型的变体,不仅适用于可验证性,还提高了维护性,它同样被序列化为String。定义形如
```typescript
enum Unit {
MM
mm
}
```
MM代表米做单位,mm代码毫米做单位。
2、对象(Ojbect)
> 组成Schema最常用的是对象类型,它包含各种字段,定义如:
```typescript
type User{
name: String
sex: String
intro: String
}
```
以上定义了一个User对象,包含name(名字)、sex(性别)、intro(介绍)属性字段,而这些属性字段都是标量String类型,当然属性也可以是对象类型。
3、集合(List)
> GraphQL规范中的集合只有List一种,它是有序的用 [] 表示,如
```typescript
type User{
name: String
sex: String
intro: String
skills: [String]
}
```
skills(技能)就是一个list集合,其实更像是一个数组。
4、非空/Null(Non-Null)
> 在类型后使用 ! 声明 非空/Null,如
```typescript
type Query {
user(id:Int!):User
}
type User{
name: String!
sex: String
intro: String
skills: [String]!
}
```
> 在类型后使用 ! 声明 非空/Null,如 (:8001)
```typescript
query{
user {
name
sex
intro
}
}
```
> 则会反馈异常
```typescript
{
"errors": [
{
"message": "Field \"user\" argument \"id\" of type \"Int!\" is required but not provided.",
"locations": [
{
"line": 2,
"column": 3
}
]
}
]
}
```
> 而实体类型User的name为非null,一但服务端返回的数据name为null,则反馈为
```typescript
{
"data": {
"user": null
}
}
```
> 然而需要注意区别 skills: [String]! 与 skills: [String!] ,前者是集合不能为null,后者是集合元素不能为null。
5、模式(Schema)
> GraphQL的Schema用于生成文档、格式定义与校验等,除了自定义的类型如上文中的User,还有两个特殊的类型Query(查询)和Mutation(维护)。如增查改删(CRUD),增改删属于后者,查属于前者。 (:8001)
```typescript
schema {
query: Query
mutation: Mutation
}
```
> 一个Schema可以没有mutaion,但必须有query。
6、查询(Query)
> Query目的是获取数据。格式
7、维护(Mutation)
> Mutataion是用来维护数据的,格式和查询类似
8、参数(Argument)
> 客户端的查询语句中对象和字段都可以传入参数,方便精确控制服务返回结果,查询语句如 (:8002)
```typescript
{
user(id:1) {
name
sex
intro
skills
stature(unit:MM)
}
}
```
> 服务返回数据
```typescript
{
"data": {
"user": {
"name": "James",
"sex": "男",
"intro": "zhaiqianfeng的英文名",
"skills": [
"Linux",
"Java",
"nodeJs",
"前端"
],
"stature": 1.8
}
}
}
```
> 服务端是靠resolver来解析实现的,后文描述。
9、 指令(Directive)
>客户端可以使用两种指令skip和include,可以用与字段(field)、片段(fragment),格式和含义如下:
>* field/fragment @skip(if: $isTrue) 当$isTrue为真(true)时不查询field或不使用fragment;
>* field/fragment @include(if: $isTrue) 当$isTrue为真(true)时查询field或使用fragment;
> 而参数变量是通过query或mutation传递的;变量形如$withName:Boolean!,以$开头,以类型结尾,类型必须是标量(scalar)、枚举(enum)或输入类型(input)。如query为 (:8005)
```typescript
query(
$noWithDog:Boolean!,
$withName:Boolean!,
$withFish:Boolean!
){
animals{
name @include(if:$withName)
... dogQuery @skip(if:$noWithDog)
... on Fish @include(if:$withFish){
tailColor
}
}
}
fragment dogQuery on Dog{
legs
}
```
在query定义中分别定义了$noWithDog(是否带着狗狗)、$withName(是否带着Name)、$withFish(是否带着鱼儿)变量,传入的参数变量为
```typescript
{
"noWithDog": true,
"withName": true,
"withFish": true
}
```
查询的结果如
```typescript
{
"data": {
"animals": [
{
"name": "dog"
},
{
"name": "fish",
"tailColor": "red"
}
]
}
}
```
10、输入(Input)
>上面的参数(Argument)我们使用是标量,但当使用Mutation来更新数据时,你可能更喜欢传入一个复杂的实体Object,GraphQL使用input关键字来到定义输入类型,不直接用Object是为了避开循环引用、接口或联合等一些不可控的麻烦,特别是input类型不能像Object那样带参数的。如
```typescript
input UserInput {
name: String!
sex: String
intro: String
skills: [String]!
}
```
> 定义了一个输入类型UserInput,字段有name、sex、intro和skills.
11、接口(Interface)
> 类似其他语言,GraphQL也有接口的概念,方便查询时返回统一类型,接口是抽象的数据类型,因此只有接口的实现才有意义,如
```typescript
interface Animal{
name: String!
}
type Dog implements Animal{
name: String!
legs: Int!
}
type Fish implements Animal{
name: String!
tailColor: String!
}
```
> 上面的接口Animal有两个实现:Dog和Fish,它们都有公共字段name。服务端的查询Schema可以是类似如下的定义
```typescript
type Query {
animals:[Animal]!
}
```
> 而客户端的查询需要使用下面的Fragment,稍后展示。
12、联合(Union)
> 联合查询,类似接口式的组合,但不要求有公共字段,使用union关键字来定义,如
```typescript
type Dog{
chinaName: String!
legs: Int!
}
type Fish{
englishName: String!
tailColor: String!
}
union Animal = Dog | Fish
```
> 则Animal可以是Dog也可以是Fish,服务端定义查询可以和上面的接口相同,客户端查询也需要使用Fragment,稍后展示。
13、 片段(Fragment)
> Fragment分为内联和外联,两种都是用于选择主体,而后者还可以共用代码。如上面的接口示例,Dog和Fish都是接口Animal的实现,有接口的公共字段name,简单的内联 (:8003)
```typescript
{
animals{
name
... on Dog{
legs
}
... on Fish{
tailColor
}
}
animalSearch(text:"dog"){
name
... on Dog{
legs
}
... on Fish{
tailColor
}
}
}
```
> 如果是Dog则查询legs字段,如果是Fish则查询tailColor字段,这种内联如果单个查询则比较方便,但多个查询则使用外联更简洁,如下
```typescript
{
animals{
... animalName
... dogLegs
... fishTail
}
animalSearch(text:"dog"){
... animalName
... dogLegs
... fishTail
}
}
fragment animalName on Animal {
name
}
fragment dogLegs on Dog{
legs
}
fragment fishTail on Fish{
tailColor
}
```
> 在联合(union)查询,因为没有公共字段,使用fragment示例如下 (:8004)
```typescript
{
animals{
... on Dog{
chinaName
legs
}
... on Fish{
englishName
tailColor
}
}
}
```
14、别名(Alias)
> GraphQL支持别名命名,这对于查询多个雷同的结果非常有用,格式为 aliasName: orginName 如 (:8002)
```typescript
{
james:user(id:1) {
name
sex
intro
skills
MM:stature(unit:MM)
mm:stature(unit:mm)
}
zhaiqianfeng:user(id:0) {
name
sex
intro
skills
MM:stature(unit:MM)
mm:stature(unit:mm)
}
}
```
> 以上分别把id为1、2的分别给予别名为james、zhaiqianfeng,以米(MM)、毫米(mm)为单位的身高分别给予别名MM、mm,服务端返回的数据如
```typescript
{
"data": {
"james": {
"name": "James",
"sex": "男",
"intro": "zhaiqianfeng的英文名",
"skills": [
"Linux",
"Java",
"nodeJs",
"前端"
],
"MM": 1.8,
"mm": 180
},
"zhaiqianfeng": {
"name": "zhaiqianfeng",
"sex": "男",
"intro": "博主,专注于Linux,Java,nodeJs,Web前端:Html5,JavaScript,CSS3",
"skills": [
"Linux",
"Java",
"nodeJs",
"前端"
],
"MM": 1.8,
"mm": 180
}
}
}
```
15、 解析(resolve)
> GrapQL API有一个入口点,通常称为“Root”,而查询都是由Root开始,如服务端定义一个root
```typescript
var root= {
user: {
name: 'zhaiqianfeng',
sex: '男',
intro: '博主,专注于Linux,Java,nodeJs,Web前端:Html5,JavaScript,CSS3'
}
};
```
> 在客户端就可以使用如下查询语句(:8001)
```typescript
{
user(id:1) {
name
sex
intro
}
}
```
> 其实常用的是使用resolver,resolver是一个函数,用来处理数据,GraphQL规范中resolver的原型是 function(obj,args,context) :
> * obj 代表上一个对象,一般不常用;
> * args 传入的参数;
> * context 上下文,比如session、数据库链接等;
> 比如express-graphql的是 function(args,context) ,省略了第一个obj。因此上例中你可以使用resolver来处理,比如:
```typescript
var root= {
user: function(args,context){
return {
name: 'zhaiqianfeng',
sex: '男',
intro: '博主,专注于Linux,Java,nodeJs,Web前端:Html5,JavaScript,CSS3'
}
}
};
```