GraphQL
学习一样新东西,我认为要从三个方面入手——为什么,是什么,怎么做
,所以接下来也会按照这个思路去展开
# 为什么要用 GraphQL
# 与 Restful 的区别
首先我想直接从 GraphQL 和 Restful 的区别开始讨论起,因为大多数开发者应当熟悉 Restful ,如果不清楚的话可以看我整理的另外一篇文章
个人观点
- Restful 是一种 API 规范和风格,学习成本不高,可以直接在原有的基础上去简单重构,使架构符合对应的规范
- GraphQL 官方宣称是一种 API 查询语言,即前后端都遵循 GraphQL 定义的格式去联调,有一定的学习成本,需要用到专门的第三方库去操作,旧项目迁移的话,工作量会较大。
- 从使用方式来看,Restful 是前后端都要遵循的风格规范,GraphQL 则是一套类型系统,可以理解成一个有特定格式的中间件(守门人)
# GraphQL 解决了什么问题
简单了解了下“与 Restful 的区别”,那实际 GraphQL 解决了什么问题?其实还是得从 Restful 说起,
RESTful 主要是使用 URL 的方式表达和定位资源,用 HTTP 动词来描述对这个资源的操作,这个规范其实很好,但是接口多了以后会发现很多问题
- 客户端展示的往往不只是一种资源,所以为了获取多个资源,得同时去请求多个资源的接口,如果只是这样倒还一回事,但往往我们只需要其他资源中的某个字段而已,但是却不得不获取其他资源中的不相关数据,这就会造成一定的资源浪费和数据冗余
- 在进行接口联调的时候,虽然会出一份对应的接口文档,但是在后续维护的时候,可能因为业务需要,将一个原本是 Number 型的数据变更 String 型的数据,而接口文档又不一定实时更新,导致前后端类型校验不一致,出现或大或小的页面异常后再来解决和兼容。因此会用到各种工具来进行友好的校验和提示,比如 superstruct,joi,class-validator 等工具(但是平时业务就很多,再加上写接口校验的文件,开发人天又要增加,就又要和 PM 大战了)
GraphQL 正是为了解决这些痛点而诞生了
GraphQL 并不拘泥于 url 和 http 动词的表达方式,它可以让客户端访问类似如 /api/graphql
的链接,然后只要按照 GraphQL 的类型格式去请求,客户端想要什么字段,想要什么类型数据,只要服务端支持,那客户端就可以拿到自己想要的内容。
- 使用 GraphQL 就需要用到它的
.gql
文件,只要前后端在联调时共享这份描述文件,那类型校验的问题就解决了 - 客户端需要的数据字段则根据这份描述文件去请求,只需要一个接口就可以按需拿到多个资源的字段,也就不会造成资源浪费和数据冗余
结论
GraphQL 能够解决使用 Restful 中出现的问题,但是也需要一定的学习成本,所以需要根据相关的使用场景再决定是否去使用
# GraphQL 的基本概念
既然已经知道“为什么要使用 GraphQL”,那接下来就可以了解下“GraphQL 具体是什么,基本的概念是什么”
GraphQL 的官网 有很好的入门材料,这里就简单带过,只是进行简单汇总学习。
# Resolver
Query Mutation
# Schema
Schema 翻译过来是 “模式;计划;概要”的意思
在 GraphQL 即是通过类型系统描述的一种数据结构,用法其实和 TypeScript
类型系统类似,或者说大部分类型系统都是大同小异的。
# 简单的综合示例
type Character {
name: String!
friends: [String]!
myField: [String!]
appearsIn: [Episode!]!
length(unit: LengthUnit = METER): Float
}
说明
Character
是一个 GraphQL 对象类型,表示其是一个拥有一些字段的类型。你的 schema 中的大多数类型都会是对象类型。name
和appearsIn
是 Character 类型上的字段。这意味着在一个操作 Character 类型的 GraphQL 查询中的任何部分,都只能出现 name 和 appearsIn 字段。String
是内置的标量类型之一 —— 标量类型是解析到单个标量对象的类型,无法在查询中对它进行次级选择。后面我们将细述标量类型。String!
表示这个字段是非空的,GraphQL 服务保证当你查询这个字段后总会给你返回一个值。在类型语言里面,我们用一个感叹号来表示这个特性。[String]!
表示friends
数组本身可以为空,也可以有任何空值成员,它一定返回一个数组[String!]
表示myField
数组本身可以为空,但是其不能有任何空值成员,也就是数组内不能有 null.另外和[String]!
不同的是,当myField: null
是验证通过的,[String]!
则必须要返回一个数组[Episode!]!
表示一个 Episode 数组。因为它也是非空的,所以当你查询 appearsIn 字段的时候,你也总能得到一个数组(零个或者多个元素)。且由于 Episode! 也是非空的,你总是可以预期到数组中的每个项目都是一个 Episode 对象。这也是最完善的写法length(unit: LengthUnit = METER): Float
表示这个字段可以接受一个unit
参数,,我们可以定义一个默认值 —— 如果 unit 参数没有传递,那么它将会被默认设置为 METER。GraphQL 中所有参数必须具名传递
# 标量类型
GraphQL 自带一组默认标量类型,也可以通过 scalar
关键字来自定义标量
Int
:有符号 32 位整数。Float
:有符号双精度浮点值。String
:UTF‐8 字符序列。Boolean
:true 或者 false。ID
:ID 标量类型表示一个唯一标识符,通常用以重新获取对象或者作为缓存中的键。ID 类型使用和 String 一样的方式序列化;然而将其定义为 ID 意味着并不需要人类可读型。
另外,GraphQL 也支持枚举类型,枚举类型是一种特殊的标量,它限制在一个特殊的可选值集合内
enum Someing {
NEWHOPE
EMPIRE
JEDI
}
这表示无论我们在 schema
的哪处使用了 Someing
,都可以肯定它返回的是 NEWHOPE、EMPIRE 和 JEDI 之一,类型系统也会对此进行验证。
# 接口
跟许多类型系统一样,GraphQL 支持接口。一个接口是一个抽象类型,它包含某些字段,而对象类型必须包含这些字段,才能算实现了这个接口
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
}
当你要返回一个对象或者一组对象,特别是一组不同的类型时,接口就显得特别有用。
如果要查询一个只存在于特定对象类型上的字段,你需要使用内联片段,不然会报错
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
}
}
# 联合类型
union SearchResult = Human | Droid | Starship
用联合类型定义 SearchResult
,任何返回一个 SearchResult 类型的地方,都可能得到一个Human、Droid 或者 Starship
。注意,联合类型的成员需要是具体对象类型;你不能使用接口或者其他联合类型来创造一个联合类型。
如果你需要查询一个返回 SearchResult 联合类型的字段,那么你得使用条件片段才能查询任意字段。由于 Human
和 Droid
共享一个公共接口(Character),你可以在一个地方查询它们的公共字段,而不必在多个类型中重复相同的字段,但是仍然需要指定在 Starship
上,否则它不会出现在结果中,因为 Starship 并不是一个 Character!
{
search(text: "an") {
__typename
... on Character {
name
}
... on Human {
height
}
... on Droid {
primaryFunction
}
... on Starship {
name
length
}
}
}
_typename
字段解析为 String
,它允许你在客户端区分不同的数据类型。
# 输入类型
目前为止,我们只讨论过将例如枚举和字符串等标量值作为参数传递给字段,但是你也能很容易地传递复杂对象。这在变更(mutation)中特别有用,因为有时候你需要传递一整个对象作为新建对象。在 GraphQL schema language 中,输入对象看上去和常规对象一模一样,除了关键字是 input 而不是 type
input ReviewInput {
stars: Int!
commentary: String
}
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
输入对象类型上的字段本身也可以指代输入对象类型,但是你不能在你的 schema 混淆输入和输出类型。输入对象类型的字段当然也不能拥有参数。