GraphQL

---

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'

        }

    }

};

```

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342