GraphQL技术怎样?有什么缺点?
【引言】
GraphQL是一种开源的数据查询和操作语言,是一种用于API的数据查询和操作的语言,也是一种利用现有数据完成查询的动态理念。 GraphQL于2012年由Facebook内部开发,2015年公开发布。2018年11月7日,GraphQL项目从Facebook转到了新成立的GraphQL基金会,由非营利性的Linux基金会主持管理。自2012年以来,GraphQL的崛起一直遵循着GraphQL的创建者Lee Byron所制定的推广时间表,并准确无误。Byron的目标是让GraphQL在Web平台上无所不在。
它提供了一种开发Web API的方法,并与REST和其他Web服务架构进行了比较和对比。它允许客户端定义所需的数据结构,而服务器返回的数据结构也是一样的,因此可以防止过量的数据被返回,但这对查询结果的web缓存的有效性有影响。查询语言的灵活性和丰富性也增加了程序设计的复杂度,对于简单的API来说,可能并不值得。它由类型系统、查询语言和执行语义、静态验证和类型自省组成。
GraphQL 支持读取、写入(包括突变)和订阅数据的变化(实时更新--最常见的是使用 WebHooks )。
GraphQL服务器适用于多种语言,包括Haskell、JavaScript、Perl、Python、Ruby、Java、C#、Scala、Go、Elixir、Erlang、PHP、R和Clojure。
2018年2月9日,GraphQL Schema Definition Language (SDL)成为规范的一部分。
GraphQL 规范最新的版本发布在 https://graphql.github.io/graphql-spec/。
最新的规范草案可以在 https://graphql.github.io/graphql-spec/draft/
找到。
之前发布的GraphQL规范可以在与其发布标签匹配的permalinks中找到, 例如,https://graphql.github.io/graphql-spec/October2016/。
【概览】
GraphQL是Facebook创建的API查询语言。
目标受众不是客户端开发者,而是那些已经对构建自己的GraphQL服务程序和工具感兴趣的人。
为了被广泛采用,GraphQL必须提供针对各种后端、框架和语言的支持,这将需要跨项目和组织的协作努力。
GraphQL 由类型系统、查询语言和执行语义、静态验证和类型自省组成,下面会分别介绍这些部分:
下面的例子中,我们会用GraphQL来查询《星球大战》三部曲中的人物和位置信息。
【类型系统】
任何GraphQL实现的核心是描述它可以返回哪些类型的对象,在GraphQL类型系统中描述并在GraphQL Schema中返回。
对于我们的Star Wars例子,GraphQL.js中的starWarsSchema.js文件定义了这个类型系统。
系统中最基本的类型将是Human,代表着像Luke、Leia和Han这样的角色。在我们的类型系统中,所有的人类都会有一个名字,所以我们定义Human类型有一个名为 "name "的字段。我们定义 "name "字段为不可空的字符串。使用我们将在整个规范和文档中使用的速记符号,我们将把人类模型定义为:
type Human {
name: String
}
这个速记词方便描述类型系统的基本状况;JavaScript实现的功能比较齐全,可以对类型和字段进行记录。它还设置了类型系统和底层数据之间的映射;对于GraphQL.js中的一个测试用例来说,底层数据是一组JavaScript对象,但在大多数情况下,支持的数据将通过一些服务来访问,而这个类型系统层将负责从类型和字段到那个服务的映射。
在许多API中,甚至在GraphQL中,一个常见的模式是给对象一个ID,可以用来访问这个对象。所以,让我们把这个添加到我们的Human类型中,此外我们还要添加另一个字符串: homePlanet。
type Human {
id: String
name: String
homePlanet: String
}
既然我们讨论的是《星球大战三部曲》,那么描述一下每个角色出现的剧集是很有用的。为此,我们先定义一个枚举,列出三部曲:
enum Episode { NEWHOPE, EMPIRE, JEDI }
现在我们要在Human中添加一个字段,描述他们参加过的剧集。是一个剧集列表:
type Human {
id: String
name: String
appearsIn: [Episode]
homePlanet: String
}
现在,我们来介绍另一种类型,Droid:
type Droid {
id: String
name: String
appearsIn: [Episode]
primaryFunction: String
}
现在我们有两种类型了! 让我们在这两种类型之间增加一种关联方式:人类和机器人都有朋友,人类和机器人也都可以做朋友。
人类和机器人之间有共同的属性;它们都有ID、名字,以及出现的剧集。
我们添加一个接口Character,让Human和Droid从这里派生。有了这些之后,我们就可以添加好友字段,也即返回一个Character列表。
我们的类型系统就成了这样:
enum Episode { NEWHOPE, EMPIRE, JEDI }
interface Character {
id: String
name: String
friends: [Character]
appearsIn: [Episode]
}
type Human implements Character {
id: String
name: String
friends: [Character]
appearsIn: [Episode]
homePlanet: String
}
type Droid implements Character {
id: String
name: String
friends: [Character]
appearsIn: [Episode]
primaryFunction: String
}
这里我们需要问一个问题,就是这些字段中的任何一个字段是否可以返回null? 默认情况下,null是GraphQL中任何类型的允许值,因为获取数据以完成GraphQL查询经常需要与不同的服务对话,而这些服务有可能是可用的,也可能是不可用的。如果我们想把某个类型标记为不能为空,我们可以通过在类型后添加一个"!"来表示。
注意,在实现中,我们可能把一些字段保留为nullable,这样我们就有了灵活性,最终可以判断返回null来表示后端错误,同时也告诉客户端发生了错误。
我们尝试把id设定为不为空字段:
enum Episode { NEWHOPE, EMPIRE, JEDI }
interface Character {
id: String!
name: String
friends: [Character]
appearsIn: [Episode]
}
type Human implements Character {
id: String!
name: String
friends: [Character]
appearsIn: [Episode]
homePlanet: String
}
type Droid implements Character {
id: String!
name: String
friends: [Character]
appearsIn: [Episode]
primaryFunction: String
}
接下来我们需要定义一个入口来访问类型系统,实际上就是一个查询数据类型。
这个类型的名称按照惯例是 Query,它描述了我们公共的、顶层API。这个例子中的查询类型是这样的:
type Query {
hero(episode: Episode): Character
human(id: String!): Human
droid(id: String!): Droid
}
在这个例子中,有三个操作字段:
l hero 返回《星球大战》三部曲中的英雄人物;它有一个可选的参数,允许我们获取特定剧集的英雄人物。
l human有一个非空字符串ID作为查询参数,返回人类。
l droid有一个非空字符串ID作为查询参数,返回机器人人。
这些字段展示了类型系统的另一个特点,即字段可以指定参数来配置它们的结果。
当我们将整个类型系统打包在一起,将上面的查询类型定义为查询的入口点,这就创建了一个GraphQL的设计定义。
查询语法
GraphQL查询准确的描述了调用者需要获取什么数据。
简单查询
我们来看例子:
query HeroNameQuery {
hero {
name
}
}
上面这个查询的名字描述了很清晰的目的,就是要查询一个名字。其返回结果大概是这样的:
{
"hero": {
"name": "R2-D2"
}
}
更多数据查询
再来看另一个查询:
query HeroNameAndFriendsQuery {
hero {
id
name
friends {
id
name
}
}
}
这个查询希望查询到id, name以及其朋友的id和name。返回结果大概这样:
{
"hero": {
"id": "2001",
"name": "R2-D2",
"friends": [
{
"id": "1000",
"name": "Luke Skywalker"
},
{
"id": "1002",
"name": "Han Solo"
},
{
"id": "1003",
"name": "Leia Organa"
}
]
}
}
嵌套查询
下面看一个嵌套查询的例子:
query NestedQuery {
hero {
name
friends {
name
appearsIn
friends {
name
}
}
}
}
返回结果:
{
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker",
"appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
"friends": [
{ "name": "Han Solo" },
{ "name": "Leia Organa" },
{ "name": "C-3PO" },
{ "name": "R2-D2" }
]
},
{
"name": "Han Solo",
"appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
"friends": [
{ "name": "Luke Skywalker" },
{ "name": "Leia Organa" },
{ "name": "R2-D2" }
]
},
{
"name": "Leia Organa",
"appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
"friends": [
{ "name": "Luke Skywalker" },
{ "name": "Han Solo" },
{ "name": "C-3PO" },
{ "name": "R2-D2" }
]
}
]
}
}
指定ID查询
query FetchLukeQuery {
human(id: "1000") {
name
}
}
或者:
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
返回结果:
{
"human": {
"name": "Luke Skywalker"
}
}
指定ID的多个查询
query FetchLukeAndLeiaAliased {
luke: human(id: "1000") {
name
}
leia: human(id: "1003") {
name
}
}
结果:
{
"luke": {
"name": "Luke Skywalker"
},
"leia": {
"name": "Leia Organa"
}
}
特殊字段:__typename
下面来看特殊字段的例子:
query CheckTypeOfR2 {
hero {
__typename
name
}
}
结果:
{
"hero": {
"__typename": "Droid",
"name": "R2-D2"
}
}
验证
通过使用类型系统,可以预先确定GraphQL查询是否有效。服务器和客户端之间如果创建了一个无效查询,类型系统就可以有效地通知开发人员,而不需要运行时才报错。
上面我们列出了一些有效的查询例子,接下来我们看看无效的查询例子:
字段未定义
# INVALID: favoriteSpaceship不存在于Character上。
query HeroSpaceshipQuery {
hero {
favoriteSpaceship
}
}
在最基础字段(标量)下再查询
# INVALID: name 已经是最基础的字段了,不能再进行子字段查询。
query HeroFieldsOnScalarQuery {
hero {
name {
firstCharacterOfName
}
}
}
片段定义
query DroidFieldInFragment {
hero {
name
...DroidFields
}
}
fragment DroidFields on Droid {
primaryFunction
}
上面这种查询是有效的,但是如果这个片段没有被多次调用的话,我们可以匿名定义:
query DroidFieldInInlineFragment {
hero {
name
... on Droid {
primaryFunction
}
}
}
上面的查询中,hero没有primaryFunction字段定义,但是Droid有这个字段,通过片段声明可以使这个查询有效。
自省
这项功能允许我们询问GraphQL支持哪些查询。
缺省询问
比如对上面的系统,我们可以这样询问:
query IntrospectionTypeQuery {
__schema {
types {
name
}
}
}
得到的结果类似于这样:
{
"__schema": {
"types": [
{
"name": "Query"
},
{
"name": "Character"
},
{
"name": "Human"
},
{
"name": "String"
},
{
"name": "Episode"
},
{
"name": "Droid"
},
{
"name": "__Schema"
},
{
"name": "__Type"
},
{
"name": "__TypeKind"
},
{
"name": "Boolean"
},
{
"name": "__Field"
},
{
"name": "__InputValue"
},
{
"name": "__EnumValue"
},
{
"name": "__Directive"
}
]
}
}
上面类型很多,我们分一下类:
l Query, Character, Human, Episode, Droid - 这些是我们在类型系统中定义的。
l String, Boolean - 这些是类型系统提供的内置标量。
l __Schema, __Type, __Type, __TypeKind, __Field, __InputValue, __EnumValue, __Directive - 这些都在前面加了双下划线,表示它们是自省系统的一部分。
现在我们只询问查询支持:
query IntrospectionQueryTypeQuery {
__schema {
queryType {
name
}
}
}
结果如下:
{
"__schema": {
"queryType": {
"name": "Query"
}
}
}
询问特定类型
query IntrospectionDroidTypeQuery {
__type(name: "Droid") {
name
}
}
结果:
{
"__type": {
"name": "Droid"
}
}
类别查询
query IntrospectionDroidKindQuery {
__type(name: "Droid") {
name
kind
}
}
结果:
{
"__type": {
"name": "Droid",
"kind": "OBJECT"
}
}
上面是对象类型,我们来看接口类型:
query IntrospectionCharacterKindQuery {
__type(name: "Character") {
name
kind
}
}
结果:
{
"__type": {
"name": "Character",
"kind": "INTERFACE"
}
}
字段支持查询
query IntrospectionDroidFieldsQuery {
__type(name: "Droid") {
name
fields {
name
type {
name
kind
}
}
}
}
结果:
{
"__type": {
"name": "Droid",
"fields": [
{
"name": "id",
"type": {
"name": null,
"kind": "NON_NULL"
}
},
{
"name": "name",
"type": {
"name": "String",
"kind": "SCALAR"
}
},
{
"name": "friends",
"type": {
"name": null,
"kind": "LIST"
}
},
{
"name": "appearsIn",
"type": {
"name": null,
"kind": "LIST"
}
},
{
"name": "primaryFunction",
"type": {
"name": "String",
"kind": "SCALAR"
}
}
]
}
}
无名字段,查类型
像列表类型字段没有名字,我们可以查看类型ofType。
query IntrospectionDroidWrappedFieldsQuery {
__type(name: "Droid") {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}
结果:
{
"__type": {
"name": "Droid",
"fields": [
{
"name": "id",
"type": {
"name": null,
"kind": "NON_NULL",
"ofType": {
"name": "String",
"kind": "SCALAR"
}
}
},
{
"name": "name",
"type": {
"name": "String",
"kind": "SCALAR",
"ofType": null
}
},
{
"name": "friends",
"type": {
"name": null,
"kind": "LIST",
"ofType": {
"name": "Character",
"kind": "INTERFACE"
}
}
},
{
"name": "appearsIn",
"type": {
"name": null,
"kind": "LIST",
"ofType": {
"name": "Episode",
"kind": "ENUM"
}
}
},
{
"name": "primaryFunction",
"type": {
"name": "String",
"kind": "SCALAR",
"ofType": null
}
}
]
}
}
询问描述文档
query IntrospectionDroidDescriptionQuery {
__type(name: "Droid") {
name
description
}
}
结果:
{
"__type": {
"name": "Droid",
"description": "A mechanical creature in the Star Wars universe."
}
}
进一步的,我们可以使用自省系统创建更强大的文档系统,从而丰富编码人员的开发体验。
【小结】
上面我们对GraphQL从类型系统,查询语法,验证和自省功能进行了学习。
接下来我们看看GraphQL有什么缺点:
缺陷
GraphQL的主要缺点是它使用单一的端点,而不是遵循HTTP规范进行缓存。而网络层面的缓存很重要,它可以减少服务器的流量。
此外,对于简单的应用程序来说,这并不是好的解决方案,因为它增加了复杂度(类型、查询、验证、自省), 而使用REST可以更简单地完成的任务。
- 点赞
- 收藏
- 关注作者
评论(0)