What ? When ? Where ? Why ? How ?
GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时。
GraphQL 包括两部分:定义的类型和类型上的字段(Schema),每个类型上的每个字段的解析函数(Resolvers)
1. GraphQL 是强类型的 Schema 。是每个 GraphQL API 的基础,它清晰的定义了每个 API 支持的操作,包括输入的参数和返回的内容
2. 按需获取。从 API 获取想要的数据,不必依赖 REST 端口返会的固定数据结构。获取的结果是可预测的。解决了 Overfetching 和 Underfetching
- Overfetching 意味着前端得到了实际不需要的数据,造成了性能和带宽的浪费
- Underfetching 获取的数据中缺少,需要通过额外的请求去获取需要的数据。
3. GraphQL 支持快速产品开发。前端开发可以使用诸多库(APollo、Relay 或Urql),可以用缓存、实时更新UI等功能
4. GraphQL 可以组合使用。Schema 拼接是 GraphQL 的新概念之一。可以组合和链接多个 GraphQL API 合并为一个(一个 GraphQL API 可以由多个 GraphQL API 构成),避免了与多个 GraphQL 端点的通信,只需处理单个 API 端点,而协调与各种服务通信的复杂性从前端隐藏了。
5. 完善的社区资源。比如graphql-tools 等,可以在 nodejs 环境中运行,同时可以与 express 和 koa 结合使用。
用不用取决于你自己,在哪里用也看你自己了
创建API的目的是使自己的软件具有可以被其他外部服务集成的能力。即使你的程序被单个前端程序所使用,也可以将此前端视为外部服务,为此,当通过API为两者之间提供通信时,你能够在不同的项目中工作。
如果你在一个大型团队中工作,可以将其拆分为创建前端和后端团队,从而允许他们使用相同的技术,并使他们的工作更轻松。
GraphQL 查询能够遍历相关对象及其字段,使得客户端可以一次请求查询大量相关数据,而不像传统 REST 架构中那样需要多次往返查询。
{
hero {
name
# 查询的备注
frinds {
name
}
}
}
# 最后的结果结构
{
data: {
hero:{
name:"xxxx",
friends:[
{name:'yyy'},
{name:'zzzz'}
]
}
}
}
在查询的过程中,我们还会根据我们传递的参数来进行查询
每一个字段和嵌套对象都能有自己的一组参数,从而使得 GraphQL 可以完美替代多次 API 获取请求。甚至你也可以给 标量(scalar)字段传递参数,用于实现服务端的一次转换,而不用每个客户端分别转换。
# 基本的传参
{
hero(id: 1){
name
}
}
# 转换传递过来的数据
{
hero(id: 1){
name
height(unit: FOOT) # 结果:5.2245 这样的浮点数 FOOT 是一个枚举类型,之后会讲到
}
}
我们会遇到这样一种情况,就是无法通过不同参数来查询相同的字段,因此这里我们就用到了别名(Aliases)
# 这样的情况下我们就无法区分结果了
{
hero(id: 1) { id, name, nickname, age }
hero(id :2) { id, name, nickname, age }
}
# 因此我们这里就可以用到别名
{
hero1: hero(id: 1) { id, name, nickname, age }
hero2: hero(id :2) { id, name, nickname, age }
}
# -----> 结果就通过别名进行区分了
{
data: {
hero1:{ name: 'xxxx', ...},
hero2:{ name: 'zzzz', ...}
}
}
我们发现,上看我们在获取数据的时候,重复写了 id, name, nickname, age
这些字段,这其实还好,字段不算多,如果我们定义的一个对象是很复杂的,并且字段繁多的,我们虽然通过 CV 大法可以简单的就复制过来,但是这样的操作很 low,因此我们可以想到一种方式,你就是把重复的字段进行一个抽离封装。graphql 中就提供了相应的处理方式片段(Fragments)
注:片段不能引用其自身或者创建回环,因为会导致结果无边界
{
hero1: hero(id: 1) { ...person }
hero2: hero(id :2) { ...person }
}
fragment person on Charactor {
id
name
nickname
age
}
# 在片段内也是可以使用变量的
fragment person on Charactor {
id
name
nickname
age
getFriend(id: $id){ # 变量马上就要讲了
name
}
}
上面我们也写了好些个 graphql 的查询,但是上面这些都是简写的形式,我们省略了操作类型以及查询的名称
操作类型:有三种分别是query 、 mutation 、subscription
查询名称:这个是自己定义的,为了让你的操作更加语义话,方便阅读
之前我们在获取某些特定数据的时候会传递参数,但是我们传递的参数是写死的,但是实际中我们肯定会动态的传递某些参数,因此就需要用变量的方式来动态接受相应的数据。
修改我们上面通过 id 获取相应 hero 的代码
注:当我们调用该查询的时候我们怎么把实参与形参对应起来呢?我们需要通过
$
后面的那个名字来进行匹配,就像下面的那个我们传递的时候就需要 {id: 1}
# { "graphiql": true, "variables": { "id": 1 } }
query getHeroById($id: ID!){
hero(id: $id){
name
}
}
变量定义看上去像是上述查询中的 ($id: ID!)。其工作方式跟类型语言中函数的参数定义一样。它以列出所有变量,变量前缀必须为 , 后 跟 其 类 型 , 本 例 中 为 I D ( 这 个 类 型 是 g r a p h q l 中 内 置 的 一 个 标 量 类 型 , 之 后 会 降 到 类 型 ) 所 有 声 明 的 变 量 都 必 须 是 标 量 、 枚 举 型 或 者 输 入 对 象 类 型 。 变 量 定 义 可 以 是 可 选 的 或 者 必 要 的 。 上 面 ‘ I D ‘ 后 面 跟 的 ‘ ! ‘ 表 示 是 必 要 的 , 如 果 没 有 ‘ ! ‘ 表 示 是 可 选 的 。 我 们 也 可 以 给 这 个 参 数 一 个 默 认 值 , 给 默 认 值 的 形 式 就 和 E S 6 中 的 方 式 是 一 样 的 ‘ ( ,后跟其类型,本例中为 ID(这个类型是 graphql 中内置的一个标量类型,之后会降到类型) 所有声明的变量都必须是标量、枚举型或者输入对象类型。 变量定义可以是可选的或者必要的。上面 `ID` 后面跟的 `!` 表示是必要的,如果没有 `!` 表示是可选的。 我们也可以给这个参数一个默认值,给默认值的形式就和 ES6 中的方式是一样的 `( ,后跟其类型,本例中为ID(这个类型是graphql中内置的一个标量类型,之后会降到类型)所有声明的变量都必须是标量、枚举型或者输入对象类型。变量定义可以是可选的或者必要的。上面‘ID‘后面跟的‘!‘表示是必要的,如果没有‘!‘表示是可选的。我们也可以给这个参数一个默认值,给默认值的形式就和ES6中的方式是一样的‘(id: ID! = 1)`
变量让我们可以避免手动传值,但是我们有时候也需要一个方式来动态的改变我们的查询结果,这时候就引入了一个概念 —— 指令:可以附着在字段或者片段包含的字段上,然后以任何服务端期待的方式来改变查询的执行
graphql 核心规范包含了两个指令: @include 和 @skip
* @include(if: Boolean) 仅在参数为 true 时,包含此字段。
* @skip(if: Boolean) 如果参数为 true ,跳过此字段。
query getHeroById($id: ID!){
hero(id: $id){
name
friends @include(if: $id > 2){
name
}
}
}
Graphql 也允许你自己定义指令,来完成某些操作。
上面也说到了,操作类型有三种,我们刚才用的都是 query 的,现在我们来了解一下 mutation,我们在实际中肯定不仅仅只是查询的操作,CRUD 这四种操作是无可避免的,我们就来讲讲变更数据。就如同查询一样,如果任何变更字段返回一个对象类型,你也能请求其嵌套字段。获取一个对象变更后的新状态也是十分有用的。
# 标量方式传参
mutation createHero($name: name, $age: age, $gender: gender){
createAHero(name: $name, age: $age, $gender: gender){
id
name
age
gender
}
}
# 输入对象类型方式传参
mutation createHero($userInfo: userInfo){
createAHero(userInfo: $userInfo){
id
name
age
gender
}
}
# userInfo
{
name: 'zs',
age: 12,
gender: 'male'
}
查询和变更之间名称之外的一个重要区别是:查询字段时,是并行执行,而变更字段时,是线性执行,一个接着一个。
这意味着如果我们一个请求中发送了两个 createHero 变更,第一个保证在第二个之前执行,以确保我们不会出现竞态。
内联片段(inline fragment)
GraphQL schema 具备定义接口和联合类型的能力,如果你查询的字读啊返回的是接口或者联合类型,那么你可能需要使用内联片段来取出下层具体类型的数据
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
这个查询中,hero 字段返回 Character 类型,取决于 episode 参数,其可能是 Human 或者 Droid 类型。在直接选择的情况下,你只能请求 Character 上存在的字段,譬如 name。
如果要请求具体类型上的字段,你需要使用一个类型条件内联片段。因为第一个片段标注为 … on Droid,primaryFunction 仅在 hero 返回的 Character 为 Droid 类型时才会执行。同理适用于 Human 类型的 height 字段。
具名片段也可以用于同样的情况,因为具名片段总是附带了一个类型。
某些情况下你并不知道你从 GraphQL 服务获取到的是什么类型,所以 GraphQL 允许你在查询的任何位置请求 __typename
注意是两个_
,这是一个元字段,用来获得那个位置的对象类型名称
{
search(text: "an") {
__typename
... on Human {
name
}
... on Droid {
name
}
... on Starship {
name
}
}
}
# 获取的结果
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo"
},
{
"__typename": "Human",
"name": "Leia Organa"
},
{
"__typename": "Starship",
"name": "TIE Advanced x1"
}
]
}
}
GraphQL 服务提供了不少元字段,之后会讲到