GraphQL学习之基础篇

517 阅读8分钟
原文链接: blog.5udou.cn

前言

最近在实践GraphQL,项目算是做完了,这会需要总结一下。以下三篇文章都是基于的demo代码是: graphql-todo-demo)

解读的graphql-js的版本是v14.3.0,规范是2018年6月份发布的标准

GraphQL介绍

GraphQL是给API设计的一种查询语言,一个依据已有数据执行查询的运行时,为你的API中的数据提供一种完全且容易理解的描述,使得API能够更容易的随着时间而演变,还支持强大的开发者工具。

具有以下特性:(翻译自Getting up and running with GraphQL)

  • 可描述性的:使用GraphQL,你获取的都是你想要的数据,不多也不会少; {:&.rollIn}
  • 分级性的:GraphQL天然遵循对象间的关系,通过一个简单的请求,我们可以获取到一个对象及其相关的对象,比如说,通过一个简单的请求,我们可以获取一个作者和他创建的所有文章,然后可以获取文章的所有评论;
  • 强类型的:使用GraphQL的类型系统,我们可以描述能够被服务器查询的可能的数据,然后确保从服务器获取到的数据和我们查询的一致;
  • 不做语言限制:并不绑定于某一特定的语言,实际上现在已经有一些不同的语言有了实践;
  • 兼容于任何后台:GraphQL不限于某一特定数据库,可以使用已经存在的数据,代码,甚至可以连接第三方的APIs.
  • 可自省的:GraphQL服务器能够查询架构的细节

为什么需要GraphQL

我们先来看看平时项目的一些api使用场景。

假如某个订单业务的接口在app端使用到以下数据:

{
  id,
  tradeNo,
  price
}

而同样的接口在web端用到的是如下数据:

{
  id,
  price,
  tags,
  toAddr
}

于是在我们开发中就会有这样的对话:

服务端A: 小林啊,这是xx接口返回的dto,你们网关直接使用就行了。

{
  id
  tradeNo
  price
  tags
  hasPaied
  toAddr
  fromAddr
  shopId
  ...
}

小林:好的,我将数据都透传给app app端B: 小林啊,你这接口怎么这么多字段?哪些字段是我们用得上的咧?你就不能修剪一下参数再给我们吗? 小林: 不好修剪呀,这个接口web端的童鞋也在用呀,你们就挑选自己用得到的字段就行了 app端B: ....

app端童鞋这会很无语,感觉很费解,明明我只要四个字段,你给我返回几十个字段干啥呢?那不是浪费带宽吗?

是的,这类现象就是所谓的“过度获取”。同时也暴露出网关这层,在处理同一个接口返回给不同终端的数据差异性的复杂,需要你增加额外的判断去实现,这种问题我们称之为“冗余判断”,这会导致系统维护成本增加。

那么有解决办法吗?当然有!我们反过来操作如何?由客户端来告诉网关需要哪些字段,网关这边准备完整的数据,然后有个什么东东来pick这些数据给客户端。于是乎,这个东东就是后来Facebook在2015年开源的GraphQL。

那么GraphQL除了解决刚才说两个普遍问题,还有别的其他优势吗?

of course。

以官方文档来阐述,便是:

  • 跨平台使用
  • 强类型的 GraphQL
  • GraphQL 版本化
  • GraphQL的语义化 => schema本身就是一份完美的自动生成的 API 文档

在后面的三篇文章中会逐一体现其优势。

GraphQL的类型系统

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

Scalar Type

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

  • Int:带符号的32位整数,对应 JavaScript 的 Number
  • Float:带符号的双精度浮点数,对应 JavaScript 的 Number
  • String:UTF-8字符串,对应 JavaScript 的 String
  • Boolean:布尔值,对应 JavaScript 的 Boolean
  • ID:ID 值,是一个序列化后值唯一的字符串,可以视作对应 ES 2015 新增的 Symbol

Scalar Types的JavaScript参考实现代码可以查看这里

我们可以使用scalar关键字,自定义标量类型,比如我们可以定义一个Date类型: scalar Date

高级类型

接口类型

Interface是包含一组确定字段的集合的抽象类型,实现该接口的类型必须包含interface定义的所有字段。比如:

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

对于interface的返回需要你使用inline fragments来实现,如下:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
  }
}

联合类型

Union类型非常类似于interface,但是他们在类型之间不需要指定任何共同的字段。通常用于描述某个字段能够支持的所有返回类型以及具体请求真正的返回类型。比如定义:

union SearchResult = Human | Droid | Starship

这个时候的查询语句可能是这样的:

{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

枚举类型

又称Enums,这是一种特殊的标量类型,通过此类型,我们可以限制值为一组特殊的值。比如:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

输入类型

input类型对mutations来说非常重要,在 GraphQL schema 语言中,它看起来和常规的对象类型非常类似,但是我们使用关键字input而非type,input类型按如下定义:

  # Input Type

  input CommentInput {
    body: String!
  }

为什么不直接使用Object Type呢?因为 Object 的字段可能存在循环引用,或者字段引用了不能作为查询输入对象的接口和联合类型。

数组类型和非空类型

使用[]来表示数组,使用!来表示非空。Non-Null强制类型的值不能为null,并且在请求出错时一定会报错。可以用于必须保证值不能为null的字段

对象类型

GraphQL schema最基本的类型就是Object Type。用于描述层级或者树形数据结构。比如:

type Character {
  name: String!
  appearsIn: [Episode!]!
}

GraphQL查询语法

GraphQL的一次操作请求被称为一份文档(document),即GraphQL服务能够解析验证并执行的一串请求字符串(Source Text)。完整的一次操作由操作(Operation)和片段(Fragments)组成。一次请求可以包含多个操作和片段。只有包含操作的请求才会被GraphQL服务执行。

其规范释义为:

只包含一个操作的请求可以不带OperationName,如果是operationType是query的话,可以全部省略掉,即:

{
  getMessage {
    # query也可以拥有注释,注释以#开头
    content
    author
  }
}

当query包含多个操作时,所有操作都必须带上名称。

GraphQL中,我们会有这样一个约定,Query和与之对应的Resolver是同名的,这样在GraphQL才能把它们对应起来。

Query

Query用做读操作,也就是从服务器获取数据。以上图的请求为例,其返回结果如下,可以看出一一对应,精准返回数据

{
  "data": {
    "single": [
      {
        "content": "test content 1",
        "author": "pp1"
      }
    ],
    "all": [
      {
        "content": "test content",
        "author": "pp",
        "id": "0"
      },
      {
        "content": "test content 1",
        "author": "pp1",
        "id": "1"
      }
    ]
  }
}

Field

Field是我们想从服务器获取的对象的基本组成部分。query是数据结构的顶层,其下属的allsingle都属于它的字段。

字段格式应该是这样的:alias:name(argument:value)

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

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

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

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

Argument

和普通的函数一样,query可以拥有参数,参数是可选的或必须的。参数使用方法如上图所示。

需要注意的是,GraphQL中的字符串需要包装在双引号中。

Variables

除了参数,query还允许你使用变量来让参数可动态变化,变量以$开头书写,使用方式如上图所示

变量还可以拥有默认值:

query gm($id: ID = 2) {
  # 查询数据
  single: getMessage(id: $id) {
    ...entity
  }
  all: getMessage {
    ...entity
    id
  }
}

Allases

别名,比如说,我们想分别获取全部消息和ID为1的消息,我们可以用下面的方法:

query gm($id: ID = 2) {
  # 查询数据
  getMessage(id: $id) {
    ...entity
  }
  getMessage {
    ...entity
    id
  }
}

由于存在相同的name,上述代码会报错,要解决这个问题就要用到别名了Allases。

query gm($id: ID = 2) {
  # 查询数据
  single: getMessage(id: $id) {
    ...entity
  }
  all: getMessage {
    ...entity
    id
  }
}

Fragments

Fragments是一套在queries中可复用的fields。比如说我们想获取Message,在没有使用fragment之前是这样的:

query gm($id: ID = 2) {
  # 查询数据
  single: getMessage(id: $id) {
    content
    author
  }
  all: getMessage {
    content
    author
    id
  }
}

但是如果fields过多,就会显得重复和冗余。Fragments在此时就可以起作用了。使用了Fragment之后的语法就如上图所示,简单清晰。

Fragment支持多层级地继承。

Directives

Directives提供了一种动态使用变量改变我们的queries的方法。如本例,我们会用到以下两个directive:

query gm($id: ID = 2, $isNotShowId: Boolean!, $showAuthor: Boolean!) {
  # 查询数据
  single: getMessage(id: $id) {
    ...entity
  }
  all: getMessage {
    ...entity
    id @skip(if: $isNotShowId)
  }
}

fragment entity on Message {
  content
  author @include(if: $showAuthor)
}

### 入参是:
{
  "id": 1,
  "isNotShowId": true,
  "showAuthor": false
}

@include: 只有当if中的参数为true时,才会包含对应fragment或field;

@skip:当if中的参数为true时,会跳过对应fragment或field;

结果如下:

{
  "data": {
    "single": [
      {
        "content": "test content 1"
      }
    ],
    "all": [
      {
        "content": "test content"
      },
      {
        "content": "test content 1"
      }
    ]
  }
}

Mutation

传统的API使用场景中,我们会有需要修改服务器上数据的场景,mutations就是应这种场景而生。mutations被用以执行写操作,通过mutations我们会给服务器发送请求来修改和更新数据,并且会接收到包含更新数据的反馈。mutations和queries具有类似的语法,仅有些许的差别。

  1. operationType为mutation
  2. 为了保证数据的完整性mutations是串形执行,而queries可以并行执行,这点在原理篇会介绍到

Subscription

Subscription是GraphQL最后一个操作类型,它被称为订阅。当另外两个由客户端通过HTTP请求发送,订阅是服务器在某个事件发生时将数据本身推送给感兴趣的客户端的一种方式。这就是GraphQL 处理实时通信的方式。

demo中我们实现了TodoList的发布订阅功能,在语法上和别的operation类似,唯一变化的就是operation Type变为了Subscription,比如:

subscription todoSubscription($filter: String!) {
  todoChanged(filter: $filter) {
    type
    payload {
      id
      title
      complete
    }
  }
}

更多的改造实现是在服务端上,需要借助graphql-subscriptionssubscriptions-transport-ws来实现整个流程。结合express-graphql的例子可以参考:Subscriptions support

最后

至此,GraphQL的基础篇讲完了,为了加深这些基础,欢迎继续阅读第二篇文章原理篇

参考

  1. Schemas and Types
  2. //howtographql.cn/