GraphQL 官方自述文档(翻译)

原文地址:github.com/facebook/gr…

我们可以在 facebook.github.io/graphql/ 最新发布的/spec目录的markdown文件中找到 GraphQL的规范。

我们可以通过跟踪这个仓库最新的代码提交找到 facebook.github.io/graphql/dra… 中提到的最近的草案规范。

我们可以通过对应的 release tag 在固定的链接找到 GrahpQL 之前的规范。例如,facebook.github.io/graphql/Oct… , 如果直接链接到 GraphQL 规范,则最好链接到特定引用版本的带有永久标记的链接。

概览

这是一个由Facebook创建的 Api 查询语言规范。

GraphQL 的受众群体不是客户端开发者,而是那些对构建 GraphQL 开发和工具感兴趣的人。

为了被广泛采用,GraphQL 必须针对各种客户端、框架和语言,这需要跨项目和组织协作。它的规范是这项工作的协调点。

在这里可以找到需要的帮助 from the community

入门

GraphQL 有类型系统、查询语言、语义执行器、静态类型检查和类型自检组成。为了使用户可以了解其中的每一个组件,我们已经完成了一个示例用来说明 GraphQL 的每一个部分。

这个示例不是全面的,但是它可以快速说明 GraphQL 的核心概念,在深入了解 GraphQL.js之前提供一些基本信息。

我们想用查询“星球大战三部曲”中人物和地点的信息来演示这个例子。

类型系统

任何 GraphQL 的实现核心都是它可以返回对象类型的描述,在类型系统中描述并且在 Schema 中返回。

在我们“星球大战”的例子中,类型系统被定义在了此文件中 starWarsSchema.js 。

在系统中最多的基本类型是 Human,代表了人物,比如像 Luke,Leia 和 Han。在我们系统中所有的人类会有一个名字,所以我们定义 Human 类型必须有一个属性: name. 这个属性会返回一个字符串,并且我们知道这个字符串不是 null(就像每个人类都必须有一个名字一样)。所以我们定义 name 属性为 non-nullable String(非空字符串)。我们在系统规范和文档中始终使用简写符号,我们可以把 Human 类型描述为这样:

type Human {
  name: String
}
复制代码

这种简写是为了方便描述类型系统最基本的组成;Javascript 实现功能更全面,并允许记录类型和字段。 并且设置了类型系统和底层数据之间的映射关系;对于 GraphQ.js 中的测试用例,底层数据是一组 Javascript 对象 set of JavaScript objects ,但是在大多数情况下,后台数据将通过某些服务进行访问,次类型系统层将负责从类型和字段大道该服务的映射。

许多 API 中的常见模式,在 GraphQL 中,是为对象提供用于重新获取对象的 ID。所以让我们把它添加到 Human 类型中,并且我们可以添加一个 string 类型的 homeplanet 字段。

type Human {
	id:String,
  name:String,
  homePlanet:String
}
复制代码

由于我们谈论的是星球大战三部曲,因此描述每个角色出现的剧集会很有用。为此,我们首先定义一个枚举,列出三部曲中的三集。

emnu Episode { NEWHOPE, EMPORE, JEDI}
复制代码

现在我们可以在 Human 中添加一个字段来描述他们出现在哪儿。这会返回一个剧集的列表。

type Human {
	id:String,
  name:String,
  appearsIn:[Episode],
  homeplanet:String
}
复制代码

现在,让我来介绍一下另一个类型:Droid

type Droid {

  id: String

  name: String

  appearsIn: [Episode]

  primaryFunction: String

}
复制代码

现在我们有两个类型!让我们添加一种方式来联系他们:人类和机器人都有朋友。但是人类可以同时和人类和机器人交朋友。我们如何区分人类还是机器人朋友呢?

如果我们看一下,我们会返现人来和机器人之间有共同的功能:他们都有 ID 、name和 episodes。所以我们可以添加一个接口,我们的类型系统目前是这样的:

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

}
复制代码

我们或许会问一个问题,有任何字段可以返回 nul l吗?默认情况下,null 在任何GraphQL类型中都是合法的,然而  因为完成一个GraphQl查询通常需要和不同的服务端进行通信,这些服务端可能不会返回值。然而如果类型系统可以保证一个类型不会是null,这样的话我们可以把这个类型标记为非空。我们在类型后面加“!”来表示非空字段。我可以更新我们的系统以备注 id 永远不可能为空。

请注意,在我们当前的实现当中,我们可以保证任何字段都是非空的(因为我们当前的实现中有硬编码数据), 我们没有把他们标记为非空。可以想象,我们最终会用实际后端数据代替硬编码数据。通过这些配置,我们允许后端在发生错误的时候返回null,并且告知客户端后端发生了错误。

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

}
复制代码

我们错过了最后一部分:类型系统的主入口

当我们定义一个 Schema,我们定义了一个object类型,他是所有查询的基础。这个类型的名称通常被叫做 Query, 它描述了我们公共顶级API,我们在这个例子中的的 Query 类型看起来像这样:

type Query {
	hero(episode:Eposode): Character
  human(id:String): Human
  droid(id:String):Droid
}
复制代码

在这个例子中,有三个顶级操作,可以在我们的框架中被完成:

  • hero 表示 谁是星战三部曲中的英雄人物,它设置了一个可选的参数允许我们查询特定剧集的英雄
  • human 接受一个非空参数ID作为查询条件来返回人物ID
  • droid 和droids保持一致

这些字段演示了类型系统的另一个功能:特定参数的字段可以配置他们的行为。

当我们将整个类型系统打包到一起,将上面的 Query 类型定义为查询入口点,这将创建一个GraphQL Schema。

这个例子只是初步涉及到了类型系统的表层。这种规范在类型系统部分和 type 目录中详细介绍了该主题。包含实现符合规范的GraphQL 的类型系统代码。

查询语法

GraphQl 查询以声明方式描述了发行者希望从执行GraphQL 查询它们想要的数据。

在我们的星战例子中,starWarsQueryTests.js 文件是一个测试文件,它使用上面讨论的模式和一组样本数据,在starWarsData.js文件中。可以运行此测试文件来执行参考实现。

query HeroNameQuery {
 hero {
 	name
 }
}
复制代码

第一行,query HeroNameQuery 定义来一个以schema的根查询类型为开头的操作名称 HeroNameQuery。综上所述,Query 有一个 hero 字段,返回一个Character 类型。Character 有一个String类型的name字段。这个查询的结果将会是这样:

{
	"hero":{
  	"name":"R2-D2"
  }
}
复制代码

当一个GraphQL 文档定义多个操作时,指定查询关键词和一个操作名称是唯一的必要条件。所以我们可以用一个查询简写重写之前的查询:

{
	hero {
  	name
  }
}
复制代码

假设GraphQL的后端服务数据将以R2-D2来标示hero。响应继续根据请求来变化;如果我们用这个查询来请求 R2-D2 的 ID和 friends 字段:

query HeroNameAndFriendsQuery {
	hero {
  	id
    name
    friends {
    	id
      name
    }
  }
}
复制代码

然后,我们会得到想这样的响应:

{
	"hero":{
  	"id":"2001",
    "name":"R2-D2",
    "friends":[
      {
      	"id":"1000",
        "name":"Luke Skywalker"
      },
      {
      	"id":"1002",
        "name":"Han solo"
      },
      {
      	"id":"1003",
        "name":"Leia Organa"
      }
    ]
  }
}
复制代码

Graphql 一个关键的方面是它的嵌套查询能力。在以上的查询中,我们请求了 R2-D2 的 friends,但是我们可以请求更多关于他们每个对象的信息。所以,让我们构造一个查询来请求 R2-D2 的 friends ,获取他们的名称和出现的剧集,然后再次嵌套查询他们每一个的 friends。

query NestedQuery {
	hero {
  	name
    friends {
    	name
      appersIn
      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来查询 human 的方法。我们在查询用采用硬编码的ID:

query FetchLukeQuery {
	human(id:"1000") {
  	name
  }
}
复制代码

结果是:

{
	"human":{
  	"name":"Luke Skywalker"
  }
}
复制代码

或者,我们可以定义一个具有参数的查询条件:

query FetchSomeIdQuery($someId:String!) {
	human(id:$someId) {
  	name
  }
}
复制代码

这个查询现在有一个参数:$someId;为了运行它,我们必须提供那个 ID。如果我们将 $someId 设为“1000”,我们将获得 Luke。如果设为“1002”,我们将获得 Han。如果我们在这里传递了一个无效的 ID ,我将会得到 human 类型的 null,表明不存在这样的对象。

注意,在默认情况下响应中的键是字段的 name。为了用不同参数查询同一个字段时简洁明了或者避免键冲突,有时候修改这个键是很有用的。

我们可以用字段别名处理这种情况,如下面的例子所示:

query FeatchLukeAliased {
	luke:human(id:"1000") {
  	name
  }
}
复制代码

我们将 human 字段的结果别名为关键字 ”luke.现在响应是:

{
	"luke":{
  	"name": "Luke Skywalker"
  }
}
复制代码

注意,键是 “luke" 而不是 “human”,就像我们之前的例子中没有使用别名一样。

这种方法在我们想要用两次同样的字段不同的参数时尤其有用,就像下面的例子:

query FetchLukeAndLeiaAliased {
	luke:human(id:"1000") {
  	name
  }
  leia:human(id:"1003") {
  	name
  }
}
复制代码

我们别名 human 的第一个结果到 luke ,并且第二个结果到 leia。所以结果是:

{
	"luke":{
  	"name":"Luke Skywalker"
  },
  "leia":{
  	"name":"Leia Organa"
  }
}
复制代码

现在我们想象,如果我们想要请求 Luke 和 Leia 的母星。我们可以这样做查询:

query DuplicateFields {
	luke:human(id:"1000") {
  	name
    homePlanet
  }
  leia:human(id:"1003") {
  	name
    homePlanet
  }
}
复制代码

但是我们已经可以看到这种方法会变得笨重,由于我们不得不添加新的字段到所有的查询部分。我们可以用提取公共字段到一个片段,并且将这个片段包含到查询中代替,像这样:

query useFragment {
	luke:human(id:"1000") {
  	...HumanFragment
  }
  leia:human(id:"1003") {
  	...HumanFragment
  }
}
frament HumanFragment on Human {
	name
  homePlanet
}
复制代码

两个查询的结果是:

{
	"luke":{
  	"name":"Luke Skywalker",
    "homePlanet":"tatooine"
  },
  "leia":{
  	"name":"Leia Organa",
    "homePlanet":"Alderaan"
  }
}
复制代码

UseFragment 和 DuplicateFields 查询都获得相同的结果,但是 UseFragment 更简洁;如果我们想要添加更多字段,我们可以添加到公共的片段而不是拷贝到多个地方。

在上面我们定义了类型系统,所以我们知道输出中每个对象的类型;可以使用定一个在每个对象尚的特殊字段 __typename 来查询类型。

query CheckTypeOfR2 {
	hero {
  	__typename
    name
  }
}
复制代码

由于R2-D2是 droid 类型,所以会返回:

{
	"hero":{
  	"__typename":"Droid",
    "name":"R2-D2"
  }
}
复制代码

我们如果想要知道返回的具体类型。这是非常有用的,因为 hero 被定义为返回 Character 接口;如果我们反而请求第五集hero呢:

query CheckTypeOfLuke {
	hero(episode:EMPIRE) {
  	__typename
    name
  }
}
复制代码

我们将会发现是 Luke

{
	"hero":{
  	"__typename":"Huamn",
    "name":"Luke Skywalker"
  }
}
复制代码

与类型系统一样,这个例子只是触及到来查询语言的表层。该规范详细介绍来关于“语言”部分详情,并且,在GraphQL.js 中的 language 目录包含来代码实现了一个规范兼容 GraphQL 查询语言解析器和词法分析器。

校验系统

通过使用类型系统,GraphQL 可以预见到一个查询是否有效。这种机制允许服务端或者客户端在无效查询被创建时有效到通知开发者,而不需要依赖运行时的检查。

在我们的星战例子中,starWarsValidationTests.js 文件中包含了一些常见的示范查询,并且是一个可运行来练习参考实现校验器的测试文件。

首先,让我们取一个复杂的校验查询。这是一个来自上面部分嵌套查询的例子,但是将重复的字段考虑在一个片段内:

query NestedQueryWithFragment {
	hero {
  	...NameAndAppearances
    friends{
    	...NameAndAppearances
      friends{
      	...NameAndAppearances
      }
    }
  }
}
fragment NameAndAppearances on Character {
	name
  appearsIn
}
复制代码

并且这个查询是有效的。让我们看一写无效的查询。

当我们查询字段时,我们必须查询一个在给定类型中存在的字段。例如 hero 返回 Character 类型,我们必须在 Character 上查询一个字段。那个类型不存在 favoriteSpaceship 字段,所以这个查询:

#INVALID:favoriteSpaceship does not exist on Charcter
query HeroSpaceshipQuery {
	hero {
  	favoriteSpaceship
  }
}
复制代码

是无效的。

每当我们查询一个字段并且它返回的不是标量或者枚举,我们必须指定我们想要从字段中获取的数据。Hero 返回 Character 类型,我们一直在请求像 name 和 appearsIn 这样的字段;如果我们忽略这一点,这个查询将会变得无效。

# INVALID: hero is not a scalar, so fields are needed
query HeroNoFieldsQuery {
	hero
}
复制代码

同样,如果是标量,则查询其他的字段没有意义,这样也会将查询变得无效。

# INVALID: name is a scalar, so fields are not permitted
queryHeroFieldsOnScalarQuery {
	hero {
  	name {
    	firstCharacterOfName
    }
  }
}
复制代码

之前,有人在问题中指出一个查询只能查询类型中有的字段;当我们查询将返回 CHaracter 类型的 hero 时,我们只能查询 Character 中存在的字段。但是,如果我们像查询 R2-D2 中主要功能会发生什么?

# INVALID: primaryFunction does not exist on Character
query DriodFieldsOnCaracter {
	hero {
  	name
    primaryFunction
  }
}
复制代码

那个查询是无效的,因为 primaryFunction 不是 Character 中的字段。我们想要一种方式来表示我们希望查询 primaryFunction 如果 Character 是 Droid,否则忽律这个字段。我们可以利用我之前介绍的 fragments 来做这件事。通过设置一个被定义在 Droid 的 fragment 并包含它,我们确保我们仅查询 primaryFunction 被定义的部分。

query DriodFieldInFragment {
	hero {
  	name
    ...DriodFields
  }
}
fragment DriodFields on Driod {
	primaryFunction
}
复制代码

这个查询是有效的,但是它有点冗长。当我们多次使用时,命名片段才是有价值的,但是我们只用了一次。 相比与命名一个片段,我们可以用一个行内片段;这仍然允许我们表示我们正在查询的类型,但是不用命名一个分开的片段。

query DriodFieldsInINLineFragment {
	hero {
  	name
    ...on Driod {
    	primaryFucntion
    }
  }
}
复制代码

这仅仅是触及到了校验系统的表面;那里有许多校验规则来确保 GraphQL 查询在语义上是有意义的。该规范介绍了更多的关于校验主题的详细信息,并且validation 目录包含了代码实现一个 GraphQL 校验器的兼容规范。

自我检查

向 GraphQL  架构询问有关于它支持的查询信息通常是很有用的。GraphQL 允许我们通过自我检查系统这样做。

在我们的星战例子中,starWarsIntrospectionTests.js 文件包含了一些自我检查系统的示范查询,并且这是一个可以运行来练习参考实现自我检查系统的测试文件。

我们设计来类型系统,所以我们知道可用的数据类型,但是如果我们没有,我们可以询问 GraphQL ,通过查询 __schema 字段(总是可以查询的根数据类型)。让我们现在开始这样做并且询问什么类型是有效的。

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"

      }

    ]

  }

}
复制代码

哇哦,那是很多数据类型。他们是什么?让我对他们进行分组:

  • Query, Character, Human, Episode, Droid - 这些在我们类型系统中定义的。

  • String, Boolean - 这些是类型系统提供的标准类型。

  • __Schema, __Type, __Field, __InputValue, __EnumValue,__Directive - 这些都是以双下划线开头,表示他们是自我检查系统的一部分。

现在,让我们尝试并且找出一个开始有效查询的部分。

query InstrospectionQueryTypeQuery {
	__schema {
  	queryType {
    	name
    }
  }
}
复制代码

我们得到的结果是:

{
	"__schema":{
  	"queryType":{
    	"name":"Query"
    }
  }
}
复制代码

而且那个是符合我们在类型系统所介绍的:Query 类型是我们开始的地方。注意这里的命名是按照惯例的。我们可以命名我们的Query类型 否则,如果我们指定它,它仍然会在这里返回 作为查询的起始类型。但是命名为Query是有用的 惯例。

检查一个具体的类型通常是有用的。让我们看看 Droid 类型:

query InstrospectionDroidTypeQuery {
	__type(name:"Droid") {
  	name
  }
}
复制代码

我们会得到结果:

{
	"type":{
  	"name":"Droid"
  }
}
复制代码

但是如果我们向了解更多关于 Droid 。例如,它是一个接口还是对象?

query IntrospectionDroidKindQuery {
	__tyoe(name:"Doid") {
  	name,
    kind
  }
}
复制代码

然后,我们会得到:

{

  "__type": {

    "name": "Droid",

    "kind": "OBJECT"

  }

}
复制代码

kind 返回一个 __TypeKind ,其中一个值是 OBJECT。如果我们查询 Character 呢?

query InstrospectionCharacterKindQuery {
	__type(name:"Character") {
  	name
    kind
  }
}
复制代码

然后,我们会得到:

{

  "__type": {

    "name": "Character",

    "kind": "INTERFACE"

  }

}
复制代码

我们发现它是一个接口类型。

一个对象知道什么字段是可用的很有用,所以关于 Droid 询问自我检查系统:

query InstrospectionDroidFiedldsQuery {
	__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"

        }

      }

    ]

  }

}
复制代码

它们是我们定义在 Droid 上面的字段。

id 在这里看起来有点奇怪,它没有该类型的名称。那是因为它是一种 为 NON_NULL 的包装类型。如果我们查询该字段类型的 ofType,我们可以在这里找到 String 类型,告诉我们这是一个非空的字符串。

同样,friends 和 appearsIn 都没有 name,因为它们是 LIST 的包装类型。我们在这些类型中查询 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."

  }


复制代码

所以我们可以用自我检查系统来访问类型系统的文档,并且可以创建浏览器文档或者富应用 IDE 工具。

这也只是触及到来自我检查系统的表面;我们可以查询枚举值,类型实现的接口等待。我们甚至可以检查自我检查系统本省。在 Introspection 部分可以看到相关主题的更多详细信息,并且 introspection 文件包含了自我检查系统查询的兼容规范代码实现。

其它内容

这个自述文件介绍了有关于类型系统,查询执行,校验和自我检查系统的参考实现。GraphQL.js 和规范中还有更多信息,包含了执行查询,响应格式化,解释类型系统如何映射到底层实现,GraphQL 响应格式化和 GraphQL 的语法描述和实现。

转载于:https://juejin.im/post/5c66299df265da2de713387b

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值