何为GraphQL?

概要

GraphQL是一种新型的,令人兴奋的,用于特定查询和操作的API。它非常灵活并且有很多好处。 它特别适合以图形和树型为组织的数据。Facebook在2012年研发出了GraphQL并在2015年将其开源。

它快速地成长成为最热门的技术之一。许多创新公司在生产环境中使用GraphQL。在此篇教程中你将学到:

  • GraphQL的原理
  • 它如何与REST进行比较
  • 如何设计数据模式
  • 如何配置一个GraphQL服务器
  • 如何实现查询和变动
  • 和一些额外的高级主题

GraphQL的亮点在哪里?

当你的数据按层次结构或图形组织时,并且前端想要访问这个层次结构或图形的不同子集时,GraphQL最能发挥作用。 考虑一个提供NBA信息的应用程序。你有球队,球员,教练,冠军,和许多与此相关的信息。这里是一些查询的样本:

  • 目前金州勇士队名单上的球员名字是什么?
  • 华盛顿奇才首发队员的名字,身高和年龄是什么?
  • 哪个在任的教练拥有最多的总冠军?
  • 哪支球队在哪一年在此教练的执教下赢得了冠军?
  • 哪个球员赢得了最有价值队员奖?

我可以给出几百个与此类似的查询。想象一下你不得不设计一个API对前端提供所有这些查询,并且还能够在你的用户和产品经理有新查询需求的时候方便地针对新查询类型扩展此API。

这并不容易。GraphQL旨在解决这个实际的问题,它只用一个API终点就能提供无比强大的能量,很快你将会看到。

GraphQL与REST

在深入讨论GraphQL的细节之前,让我们将其与REST进行比较,谁是目前最流行的web API。

REST遵循一个以资源为导向的模型。如果我们的资源是队员,教练和球队,那么可能会有像下面这样的API终点:

  • /players
  • /players/<id>
  • /coaches
  • /coaches/<id>
  • /teams
  • /teams/<id>

通常不带id的API终点只返回一组id,带id的API终点返回一个资源的完整信息。你当然也能以其它方式设计你的API(比如/player API终点也可以返回每个队员的名字或者每个队员的所有信息)。

此方法在一个动态环境中的问题在于你无法获取充足的信息(比如你只获取了一组id但你需要更多的信息)或者得到太多的信息(比如当你只需要队员名字时你确收到队员所有的信息)。

这些都是很难解决的问题。当无法获取足够信息时,如果你拿到100个id,你将需要去执行100个独立的API调用去获取每个队员的信息。 当获取过多的信息时,你浪费了许多后台的处理时间和用来准备和传输很多不需要的数据的网络带宽。

REST有对此的解决方案。你可以设计许多定制的API终点,这些终点提供那些你正好需要的数据。但此方案没有什么扩展性。 很难去保持定制API终点的一致性。很难去继续开发定制API终点。很难去写定制API终点的文档并使用它。很难去维护定制API终点当它们的功能之间有很多的重叠。

考虑一下这些额外的终点:

  • /players/names
  • /players/names_and_championships
  • /team/starters

另一个方法是保持一小部分数量的通用API终点,但提供丰富的查询参数。这个方法避免了许多API终点的问题,但它违背了REST模型的理念。

你可以说GraphQL已将此方法用到了极致。它不是根据明确定义的资源来思考,而是根据整个资源领域的子图来进行思考。

GraphQL类型系统

GraphQL使用一种由类型和属性组成的类型系统给领域建模。每一个属性都有一个类型。属性类型可以是由GraphQL提供的基础类型的一种,像ID,字符串,布尔函数或用户自定义类型。 GraphQL图的节点是用户自定义的类型,连接节点的线是用户自定义类型的属性。

例如,如果一个“Player(球员)”类型有一个带“Team(球队)”类型的“team(球队)”属性,那么它意味着在每一个球员节点和一个球队节点之间有一条连接线。所有类型都在描述GraphQL领域对象模型的模式中定义。

这是NBA领域的一个非常简化的模式。 球员有一个名字,一个与他最相关的球队(是的,我知道球员有时会从一个球队转会到另一个球队),以及球员赢得的总冠军数量。

球队有一个名字,一队球员,以及球队赢得的冠军数量。

type Player {
    id: ID
  name: String!
    team: Team!
    championshipCount: Integer!
}
 
type Team {
    id: ID
    name: String!
    players: [Player!]!
    championshipCount: Integer!
}

还有预先定义好的进入点。 这些是Query(查询),Mutation(变动)和Subscription(订阅)。 前端通过入口点与后端进行通信,并根据需要进行定制。

这是一个简单地返回所有玩家的查询:

type Query {
    allPlayers: [Player!]!
}

感叹号表示该值不能为空值(null)。 在allPlayers查询的情况下,它可以返回一个空列表,但不能为空值。 此外,这意味着列表中的球员也不能为空值(因为它也有一个感叹号)。

设置GraphQL服务器

这是一个基于node-express的全功能GraphQL服务器。 它有一个在内存里硬编码的数据库。 通常,数据将存储在数据库中或从其它服务中获取。 数据在这里定义(如果你最喜欢的球队或球员没有在这里,我先向你道歉):

let data = {
  "allPlayers": {
    "1": {
      "id": "1",
      "name": "Stephen Curry",
      "championshipCount": 2,
      "teamId": "3"
    },
    "2": {
      "id": "2",
      "name": "Michael Jordan",
      "championshipCount": 6,
      "teamId": "1"
    },
    "3": {
      "id": "3",
      "name": "Scottie Pippen",
      "championshipCount": 6,
      "teamId": "1"
    },
    "4": {
      "id": "4",
      "name": "Magic Johnson",
      "championshipCount": 5,
      "teamId": "2"
    },
    "5": {
      "id": "5",
      "name": "Kobe Bryant",
      "championshipCount": 5,
      "teamId": "2"
    },
    "6": {
      "id": "6",
      "name": "Kevin Durant",
      "championshipCount": 1,
      "teamId": "3"
    }
  }, 
  "allTeams": {
    "1": {
      "id": "1",
      "name": "Chicago Bulls",
      "championshipCount": 6,
      "players": []
    },
    "2": {
      "id": "2",
      "name": "Los Angeles Lakers",
      "championshipCount": 16,
      "players": []
    },
    "3": {
      "id": "3",
      "name": "Golden State Warriors",
      "championshipCount": 5,
      "players": []
    }
  }
}

我使用的库有:

const express = require('express');
const graphqlHTTP = require('express-graphql');
const app = express();
const { buildSchema } = require('graphql');
const _ = require('lodash/core');

这是构建模式的代码。 请注意,我向allPlayers根查询添加了一些变量。

schema = buildSchema(`
  type Player {
    id: ID
    name: String!
    championshipCount: Int!
    team: Team!
  }
  type Team {
    id: ID
    name: String!
    championshipCount: Int!
    players: [Player!]!
  }
  type Query {
    allPlayers(offset: Int = 0, limit: Int = -1): [Player!]!
  }`)

关键的部分是:连接查询并真正地提供数据。 rootValue对象可以包含多个根。

这里只有allPlayers查询。 它从参数中提取offset(偏差)和limit(限制),以此对所有球员数据进行分切,然后根据球队ID给每个球员的球队属性赋值。 这使得每个球员都是一个嵌套对象。

rootValue = {
  allPlayers: (args) => {
    offset = args['offset']
    limit = args['limit']
    r = _.values(data["allPlayers"]).slice(offset)
    if (limit > -1) {
      r = r.slice(0, Math.min(limit, r.length))
    }
    _.forEach(r, (x) => {
      data.allPlayers[x.id].team   = data.allTeams[x.teamId]
    })
    return r
  },
}

最后,这里是graphql的API终点,传递模式和根值对象:

app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: rootValue,
  graphiql: true
}));
app.listen(3000);
module.exports = app;

把graphiql设置为true使我们能够用一个很优秀的内置在浏览器中的IDE来测试服务器。我强烈推荐使用它来测试不同的查询。

使用GraphQL的特别查询

一切都设定好了。 让我们导航到http://localhost:3000/graphql并找点乐子。

我们可以从简单的查询开始,查询一个玩家名字列表:

query justNames {
    allPlayers {
    name
  }
}

Output:
{
  "data": {
    "allPlayers": [
      {
        "name": "Stephen Curry"
      },
      {
        "name": "Michael Jordan"
      },
      {
        "name": "Scottie Pippen"
      },
      {
        "name": "Magic Johnson"
      },
      {
        "name": "Kobe Bryant"
      },
      {
        "name": "Kevin Durant"
      }
    ]
  }
}

好的。 我们在这里查询到了一些超级明星。 毫无疑问。 让我们来看看更有趣的事情:从偏移(offset)4开始,查询2名球员。 对于每个球员,请返回他们的名字,他们获得的冠军数量,球队名称以及球队赢得的冠军数量。

query twoPlayers {
    allPlayers(offset: 4, limit: 2) {
    name
    championshipCount
    team {
      name
      championshipCount
    }
  }
}


Output:
{
  "data": {
    "allPlayers": [
      {
        "name": "Kobe Bryant",
        "championshipCount": 5,
        "team": {
          "name": "Los Angeles Lakers",
          "championshipCount": 16
        }
      },
      {
        "name": "Kevin Durant",
        "championshipCount": 1,
        "team": {
          "name": "Golden State Warriors",
          "championshipCount": 5
        }
      }
    ]
  }
}

所以科比在湖人队中赢得了五次总冠军,湖人队总共赢得了16次总冠军。 凯文杜兰特在勇士队中仅赢得了一次总冠军,勇士队总共赢得了五次总冠军。

GraphQL的变动

魔术师约翰逊肯定是赛场上的魔术师。 但是如果没有他的朋友卡里姆·阿卜杜勒·贾巴尔,他是无法成功的。 让我们将卡里姆添加到我们的数据库。 我们可以定义GraphQL变动来执行操作,如添加,更新和删除图中的数据。

首先,让我们在模式中添加一个变动类型。 它看起来有点像功能签名:

type Mutation {

    createPlayer(name: String,

                 championshipCount: Int,

                 teamId: String): Player

}

然后,我们需要实现它并将其添加到根值。 该实现简单地使用查询提供的参数并向data['allPlayers']添加新对象。 它也确保我们正确地设置了球队。 最后,它返回新的球员。

createPlayer: (args) => {
  id = (_.values(data['allPlayers']).length + 1).toString()
  args['id'] = id
  args['team'] = data['allTeams'][args['teamId']]
  data['allPlayers'][id] = args
  return data['allPlayers'][id]
},

要真正地添加卡里姆,我们可以调用这个变动(mutation)并查询返回的球员:

mutation addKareem {
  createPlayer(name: "Kareem Abdul-Jabbar",
               championshipCount: 6,
               teamId: "2") {
    name
    championshipCount
    team {
      name
    }
  }
}

Output:
{

  "data": {
    "createPlayer": {
      "name": "Kareem Abdul-Jabbar",
      "championshipCount": 6,
      "team": {
        "name": "Los Angeles Lakers"
      }
    }
  }
}

这是一个关于变动(mutation)的黑暗小秘密......它们实际上与查询是完全一样的。 您可以在查询中修改数据,并且您可以仅返回来自变动(mutation)的数据。 GraphQL不会窥探你的代码。 查询和突变都可以接受参数并返回数据。 它更像是语法糖,让你的模式更具人性化。

高级主题

订阅

订阅是GraphQL的另一个杀手级的功能。 通过订阅,客户端可以订阅无论何时服务器状态发生变化都会触发的事件。 订阅是在后期被引入的,并以不同的方式通过不同的框架被实施的。

验证

GraphQL将针对模式验证每个查询或变动。 当输入数据具有复杂形态时,这会是一个巨大的胜利。 您不必编写烦人且脆弱的验证代码。 GraphQL将为您处理它。

模式自省

您可以检查和查询当前模式本身。 这会赋予您权力去动态地发现模式。 这是一个返回所有类型名称及其描述的查询:

query q {
  __schema {
    types {
      name
      description           
    }
  }

GraphQL是一个令人兴奋的新API技术,它提供了许多优于REST API的优点。在其背后有一个充满活力的社区,更不用说Facebook。 我预测它会很快成为前端的主流。 试一试, 你会喜欢的。

文章来源:https://code.tutsplus.com/zh-hans/tutorials/what-is-graphql--cms-29271

原文发布于微信公众号 - 前端达人(frontend84)

原文发表时间:2018-09-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IT技术精选文摘

GO语言版鹅厂广告交易实时平台价格解析

腾讯广告实时交易平台在向竞价胜出一方返回成交价的时候,先对价格进行TEA加密,再对密文进行BASE64编码,接收方先对BASE64解码,再对密文解密,双方事先...

1003
来自专栏章鱼的慢慢技术路

程序员算法时间空间复杂度速查表

1545
来自专栏数据结构与算法

2821 天使之城

2821 天使之城  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 黄金 Gold 题解  查看运行结果 题目描述 Descriptio...

3186
来自专栏小詹同学

爬点重口味的 。

小弟最近在学校无聊的很哪,浏览网页突然看到一张图片,对面的女孩看过来(邪恶的一笑),让人想入非非啊,一看卧槽,左边这妹子彻底赢了,这(**)这么大,还这么漂亮,...

1342
来自专栏章鱼的慢慢技术路

程序员算法时间空间复杂度速查表

2023
来自专栏区块链

逆向基础(二)

写在前面:最近在学逆向,然后很多人说不知道怎么入门,我也在摸着石头过河,然后想的发一个系列。一来分享学习的过程,大家一起进步,二来就当笔记来记录一下,加深印象。...

2006
来自专栏王大锤

iOS中的预编译指令的初步探究

4198
来自专栏木子昭的博客

Javascript是个好东西(广大人民的智慧是无穷的):

图片发自简书App 1,面向对象? 其他编程语言对于面向对象要么支持,要么不支持,而js支持原型链,具体的实现要自己动手,实现的方式也是各种流派(相当于别人家的...

2808
来自专栏数据结构与算法

洛谷P1345 [USACO5.4]奶牛的电信Telecowmunication

题目描述 农夫约翰的奶牛们喜欢通过电邮保持联系,于是她们建立了一个奶牛电脑网络,以便互相交流。这些机器用如下的方式发送电邮:如果存在一个由c台电脑组成的序列a1...

34110
来自专栏个人随笔

C# 操作 access 数据库

private staticstring connStr = @"Provider= Microsoft.Ace.OLEDB.12.0;...

44913

扫码关注云+社区

领取腾讯云代金券