graphql, GraphQL是一个查询语言和执行引擎绑定到任何后端服务

分享于 

30分钟阅读

GitHub

  繁體 雙語
GraphQL is a query language and execution engine tied to any backend service.
  • 源代码名称:graphql
  • 源代码网址:http://www.github.com/facebook/graphql
  • graphql源代码文档
  • graphql源代码下载
  • Git URL:
    git://www.github.com/facebook/graphql.git
    Git Clone代码到本地:
    git clone http://www.github.com/facebook/graphql
    Subversion代码到本地:
    $ svn co --depth empty http://www.github.com/facebook/graphql
    Checked out revision 1.
    $ cd repo
    $ svn up trunk
    
    GraphQL

    /spec 中找到了GraphQL规范,其中最新版本发布于 http://facebook.github.io/graphql/。

    可以在 http://facebook.github.io/graphql/draft/ 找到最新的草案规范,它跟踪这个仓库中对主分支的最新提交。

    可以在permalinks中找到以前版本的GraphQL规范,这些链接是 MATCH的发行标记标记。 例如 http://facebook.github.io/graphql/October2016/。 如果直接链接到GraphQL规范,最好连接到特定引用版本的标记永久性的永久性。

    概述

    这是GraphQL规范的工作草案,它是由Facebook创建的api的查询语言。

    这里规范的目标受众不是客户端开发人员,而是对构建自己的GraphQL实现和工具感兴趣的用户。

    为了广泛采用,GraphQL必须目标各种后端。框架和语言,这将需要项目和组织的协作工作。 这里规范作为这里工作的协调点。

    查找帮助从社区中找到资源

    正在启动

    GraphQL由类型系统。查询语言和执行语义。static 验证和类型自省组成,每一个概要文件都包含。 为了引导你完成这些组件,我们编写了一个例子来说明GraphQL的各个部分。

    这个例子并不全面,但是它旨在快速介绍GraphQL的核心概念,在深入更详细的规范或者 GraphQL.js 参考实现之前提供一些上下文。

    这个例子的前提是我们想使用GraphQL查询原始星球大战三部曲中的人物和位置信息。

    类型系统

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

    对于星球大战示例,GraphQL.js 中的starWarsSchema.js 文件定义了这种类型系统。

    系统中最基本的类型是 Human,它代表像 Luke。Leia和Han这样的字符。 我们类型系统中的所有人都有一个 NAME,所以定义 Human 类型来拥有一个叫做"姓名"的字段。 这返回一个字符串,我们知道它不是空( 因为所有 Human的名字都有),所以我们将"姓名"字段定义为非空字符串。 我们将使用在整个规范和文档中使用的shorthand 表示法,将人类类型描述为:

    typeHuman {
     name: String}

    这个 shorthand 很方便地描述了类型系统的基本形状;JavaScript实现更加完整,并允许类型和字段。 系统还设置了类型系统和基础数据之间的映射;在 GraphQL.js, 中,测试用例是一个JavaScript对象的集合,但是在大多数情况下,底层的数据将通过类型和字段的映射来映射到服务。

    在许多api中,并且实际上在中,一个常见的是给对象一个 ID,它可以用于对对象进行 refetch。 让我们把它添加到我们的人类类型中。 我们还将为他们的家庭星球添加一条弦。

    typeHuman {
     id: Stringname: StringhomePlanet: String}

    既然我们讨论了星战三角曲,那么描述每个字符的集合会很有用。 为此,我们首先定义一个 enum,它列出三部曲中的三个情节:

    enumEpisode { NEWHOPE, EMPIRE, JEDI }

    现在我们要向 Human 添加一个字段,描述它们在哪个Fragment中。 这将返回 Episode的列表:

    typeHuman {
     id: Stringname: StringappearsIn: [Episode]
     homePlanet: String}

    现在,让我们介绍另一种类型 Droid:

    typeDroid {
     id: Stringname: StringappearsIn: [Episode]
     primaryFunction: String}

    现在我们有两种类型 ! 让我们在它们之间加入一种方法: 人类和机器人都有朋友。 但是人类可以和人类和机器人一起成为朋友。 我们如何引用人或者机器人?

    如果我们看到,我们注意到人类和机器人之间有共同的功能;它们都有,。 因此我们将添加一个接口 Character,并使 HumanDroid 实现它。 一旦我们有了这个,我们可以添加 friends 字段,返回 Character s的列表。

    目前我们的类型系统是:

    enumEpisode { NEWHOPE, EMPIRE, JEDI }interfaceCharacter {
     id: Stringname: Stringfriends: [Character]
     appearsIn: [Episode]
    }typeHumanimplementsCharacter {
     id: Stringname: Stringfriends: [Character]
     appearsIn: [Episode]
     homePlanet: String}typeDroidimplementsCharacter {
     id: Stringname: Stringfriends: [Character]
     appearsIn: [Episode]
     primaryFunction: String}

    但是,我们可能会问,这些字段中是否有一个可以返回 null。 对于任何类型的数据,null 是允许的值,因为获取数据以满足查询常常需要与可能或者可能不可用的不同服务进行对话。 然而,如果类型系统能保证类型不是空的,那么我们可以在类型系统中将它的标记为非 Null。 我们指出在我们的shorthand 中,在类型后面添加一个""。 我们可以更新类型系统,以注意 id 永远不会空。

    注意,在当前实现中,我们可以保证更多的字段是非空的( 因为我们目前的实现有硬编码数据),我们没有将它们标记为非空。 可以想象,我们最终将我们的硬件数据替换为后端服务,这可以能不是完全可以靠的。

    enumEpisode { NEWHOPE, EMPIRE, JEDI }interfaceCharacter {
     id: String!name: Stringfriends: [Character]
     appearsIn: [Episode]
    }typeHumanimplementsCharacter {
     id: String!name: Stringfriends: [Character]
     appearsIn: [Episode]
     homePlanet: String}typeDroidimplementsCharacter {
     id: String!name: Stringfriends: [Character]
     appearsIn: [Episode]
     primaryFunction: String}

    我们丢失了最后一个部分: 输入类型系统中的入口点。

    定义模式时,我们定义了一个对象类型,它是所有查询的基础。 这个类型的NAME 按照惯例是 Query,它描述了我们的public,顶级 API。 本示例的Query 类型如下所示:

    typeQuery {
     hero(episode: Episode): Characterhuman(id: String!): Humandroid(id: String!): Droid}

    在本例中,可以在架构上执行三个顶层操作:

    • hero 返回了Star三部曲的英雄;它接受一个可选的参数,让我们可以获取一个特定情节的英雄,而不是一个特定的场景。
    • Human 接受非空字符串作为查询参数,并返回人类的ID,并返回带有该ID的人。
    • Droid 对机器人执行同样的操作。

    这些字段演示了类型系统的另一个特性,一个字段能够指定配置它的行为的参数。

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

    这个例子只是触及了类型系统的表面。 在"类型系统"部分,该规范详细介绍了这个主题,而的类型插件目录包含实现规范规范的GraphQL类型系统。

    查询语法

    GraphQL查询声明地描述了发布者希望从谁获得GraphQL查询中获取的数据。

    对于星球大战示例,GraphQL.js 存储库中的文件包含很多查询和响应。 文件是使用讨论的上面 和一组示例数据( 位于 starWarsData.js 中)的测试文件。 这里测试文件可以运行以执行参考实现。

    有关 上面 架构的示例查询是:

    queryHeroNameQuery {
     hero {
     name }
    }

    最初的行 query HeroNameQuery 使用操作查询类型( 在本例中为模式查询类型的root ) 中定义一个查询;在这种情况下,Query 为。 作为定义的上面,Query 有一个 hero 字段,它返回 Character,因此我们将查询。 然后 Character 有一个 name 字段返回 String,因此我们查询它,完成查询。 这里查询的结果将为:

    {
     "hero": {
     "name": "R2-D2" }
    }

    指定 Query 关键字和操作 NAME 仅在GraphQL文档定义多个操作时才需要。 因此,我们可以用查询 shorthand 编写前面的查询:

    {
     hero {
     name }
    }

    假设GraphQL服务器的备份数据将R2-D2作为英雄。 响应根据请求继续变化;如果我们要求r2-d2和好友使用这里查询,请执行以下操作:

    queryHeroNameAndFriendsQuery {
     hero {
     idnamefriends {
     idname }
     }
    }

    然后我们将得到这样的响应:

    {
     "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的朋友,但是我们可以询问关于这些对象的更多信息。 所以让我们构建一个询问r2-d2的朋友,得到他们的NAME 和剧集外观,然后询问他们的每一个朋友,他们的朋友和朋友的查询。

    queryNestedQuery {
     hero {
     namefriends {
     nameappearsInfriends {
     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" }
     ]
     }
     ]
     }
    }

    Query 类型 上面 定义了一种用于获取人员的方法。 我们可以通过硬编码查询中的ID来使用它:

    queryFetchLukeQuery {
     human(id: "1000") {
     name }
    }

    to

    {
     "human": {
     "name": "Luke Skywalker" }
    }

    或者,我们可以定义查询以具有查询参数:

    queryFetchSomeIDQuery($someId: String!) {
     human(id: $someId) {
     name }
    }

    这里查询现在由 $someId 参数化;若要运行该查询,必须提供该标识。 如果我们用 $someId 设置为" 1000",我们会得到 Luke ;设置为" 1002",我们会得到 Han。 如果在这里传递一个无效的ID,我们将返回 nullHuman,指示不存在这样的对象。

    注意,响应中的键是字段的NAME,默认情况下是。 更改这个密钥有时很有用,为了清晰或者避免在提取不同参数的同一个字段时出现密钥冲突。

    我们可以使用字段别名来执行这里操作,如下面的查询所示:

    queryFetchLukeAliased {
     luke: human(id: "1000") {
     name }
    }

    我们将 Human 字段的结果别名为键 luke。 现在响应是:

    {
     "luke": {
     "name": "Luke Skywalker" }
    }

    注意密钥是"luke"而不是"人类",因为我们之前的例子中没有使用别名。

    如果我们希望使用同一个字段两次不同的参数,这是特别有用的,如下面的查询所示:

    queryFetchLukeAndLeiaAliased {
     luke: human(id: "1000") {
     name }
     leia: human(id: "1003") {
     name }
    }

    我们将第一个 Human 字段的结果别名为键 luke,第二个字段为 leia。 结果是:

    {
     "luke": {
     "name": "Luke Skywalker" },
     "leia": {
     "name": "Leia Organa" }
    }

    现在想象一下我们想问Luke和leia行星的家。 我们可以用这个查询来实现:

    queryDuplicateFields {
     luke: human(id: "1000") {
     namehomePlanet }
     leia: human(id: "1003") {
     namehomePlanet }
    }

    但是我们已经看到这可能会变得笨拙,因为我们必须向查询的两部分添加新的字段。 我们可以将公共字段提取到 fragment 中,并在查询中包含 fragment,如下所示:

    queryUseFragment {
     luke: human(id: "1000") {
     ...HumanFragment }
     leia: human(id: "1003") {
     ...HumanFragment }
    }fragmentHumanFragmentonHuman {
     namehomePlanet}

    这两个查询都给出了以下结果:

    {
     "luke": {
     "name": "Luke Skywalker",
     "homePlanet": "Tatooine" },
     "leia": {
     "name": "Leia Organa",
     "homePlanet": "Alderaan" }
    }

    如果我们想增加更多的字段,但是我们可以把它添加到通用的fragment 中,而不是将它复制到多个地方。

    在这个过程中,我们定义了类型系统 上面,因此我们知道输出中每个对象的类型;查询可以使用在每个对象上定义的特殊字段 __typename 来请求该类型。

    queryCheckTypeOfR2 {
     hero {
     __typenamename }
    }

    因为R2-D2是一个机器人,这将返回

    {
     "hero": {
     "__typename": "Droid",
     "name": "R2-D2" }
    }

    因为 hero 被定义为返回一个接口,这尤其有用,所以我们可能想知道具体的具体类型是什么。 如果我们要 for V的英雄

    queryCheckTypeOfLuke {
     hero(episode: EMPIRE) {
     __typenamename }
    }

    我们会发现是 Luke,他是一个人:

    {
     "hero": {
     "__typename": "Human",
     "name": "Luke Skywalker" }
    }

    类型系统一样,本示例只是触及了查询语言的表面。 在"语言"部分,该规范详细介绍了这个主题,而的语言插件目录包含实现规范规范的GraphQL查询语言解析器和分析器。

    验证

    通过使用类型系统,可以确定GraphQL查询是有效的还是不正确的。 这样,服务器和客户端可以在创建无效查询时有效地通知开发人员,而不必依赖运行时检查。

    对于我们的星球大战示例,file starWarsValidationTests.js 包含许多演示各种invalidities的查询,它是一个可以运行的测试文件,可以运行参考验证程序。

    首先,让我们进行一个复杂的有效查询。 这是 上面 部分的NestedQuery 示例,但在 fragment 中添加了重复的字段:

    queryNestedQueryWithFragment {
     hero {
     ...NameAndAppearancesfriends {
     ...NameAndAppearancesfriends {
     ...NameAndAppearances }
     }
     }
    }fragmentNameAndAppearancesonCharacter {
     nameappearsIn}

    这个查询是有效的。 让我们来看看一些无效的查询 !

    查询字段时,必须查询存在于给定类型上的字段。 所以当 hero 返回 Character 时,我们必须查询 Character 上的字段。 类型没有 favoriteSpaceship 字段,因此这里查询:

    # INVALID: favoriteSpaceship does not exist on CharacterqueryHeroSpaceshipQuery {
     hero {
     favoriteSpaceship }
    }

    无效。

    每当我们查询字段并返回标量或者 enum 之外的东西时,我们需要指定我们想从字段返回什么数据。 Character 返回一个,并且我们在它上面请求类似 nameappearsIn的字段;如果省略,查询将无效:

    # INVALID: hero is not a scalar, so fields are neededqueryHeroNoFieldsQuery {
     hero}

    类似地,如果字段是标量,则查询它的他字段并无意义,这样做将使查询无效:

    # INVALID: name is a scalar, so fields are not permittedqueryHeroFieldsOnScalarQuery {
     hero {
     name {
     firstCharacterOfName }
     }
    }

    前面提到,查询只能查询关于类型的字段;当我们查询返回 Characterhero 时,只能查询 Character 中的字段。 如果我们想查询R2-D2s主函数,会发生什么情况?

    # INVALID: primaryFunction does not exist on CharacterqueryDroidFieldOnCharacter {
     hero {
     nameprimaryFunction }
    }

    因为 primaryFunction 不是 Character 上的字段,所以该查询无效。 我们需要一些方法,指示如果 CharacterDroid,则希望获取 primaryFunction,否则将忽略该字段。 我们可以使用前面介绍的Fragments 来执行这里操作。 通过设置 Droid 上定义的fragment 并包括它,我们确保只在定义它的primaryFunction 处查询。

    queryDroidFieldInFragment {
     hero {
     name...DroidFields }
    }fragmentDroidFieldsonDroid {
     primaryFunction}

    这个查询有效,但是有点详细;命名 Fragments 是有价值的上面,但我们只使用了这一次。 我们可以使用 inline fragment 来代替命名的fragment,但不使用命名的,但不命名单独的fragment:

    queryDroidFieldInInlineFragment {
     hero {
     name...onDroid {
     primaryFunction }
     }
    }

    这只是破坏了验证系统的表面;有一些验证规则可以确保GraphQL查询在语义上有意义。 在"验证"部分,该规范将详细介绍这个主题,而中的验证插件目录包含实现规范规范的GraphQL验证程序的代码。

    Introspection

    查询GraphQL架构的信息通常是有用的,因为它支持哪些查询。 GraphQL允许我们使用内省系统来实现 !

    对于星球大战示例,文件 starWarsIntrospectionTests.js 插件包含大量演示内省系统的查询,它是一个可以运行的测试文件,可以运行实现系统的参考内省。

    如果输入的类型系统是可用的,那么我们知道可以使用什么类型,但是如果没有,那么可以通过查询字段,查询查询的root 类型始终可用。 现在让我们这样做,并询问哪些类型可用。

    queryIntrospectionTypeQuery {
     __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" }
     ]
     }
    }

    哇,这可是很多 ! 他们是什么让我们分组:?

    • 我们在类型系统中定义了查询。人物。人物。剧集,Droid。
    • 字符串,布尔这些是在类型系统提供的标量中构建的。
    • 这都是 ,__Type,__TypeKind,__Field,__InputValue,__EnumValue,__Directive,它们前面带有一个双下划线,表示它们是内省系统的一部分。

    现在,让我们尝试找出一个好的地方来探索哪些查询是可用的。 当我们设计类型系统时,我们指定了所有查询的开始类型; !

    queryIntrospectionQueryTypeQuery {
     __schema {
     queryType {
     name }
     }
    }

    然后我们回来:

    {
     "__schema": {
     "queryType": {
     "name": "Query" }
     }
    }

    我们在类型系统部分所说的MATCHES,Query 类型就是我们要开始的地方 ! 注意,这里的命名只是按照约定,我们可以将它的命名为 Query 类型,如果将它指定为。 把它命名为 Query 是一个很有用的约定。

    检查一个特定类型通常是有用的。 让我们看一下 Droid 类型:

    queryIntrospectionDroidTypeQuery {
     __type(name: "Droid") {
     name }
    }

    然后我们回来:

    {
     "__type": {
     "name": "Droid" }
    }

    如果我们想了解更多关于机器人的信息? 例如它是接口还是对象?

    queryIntrospectionDroidKindQuery {
     __type(name: "Droid") {
     namekind }
    }

    然后我们回来:

    {
     "__type": {
     "name": "Droid",
     "kind": "OBJECT" }
    }

    kind 返回 __TypeKind enum,其中一个值是 OBJECT。 如果我们询问 Character:

    queryIntrospectionCharacterKindQuery {
     __type(name: "Character") {
     namekind }
    }

    然后我们回来:

    {
     "__type": {
     "name": "Character",
     "kind": "INTERFACE" }
    }

    我们会发现它是一个接口。

    对于一个对象来说知道哪些字段是可以用的,所以让我们来问一下关于 Droid的自省系统:

    queryIntrospectionDroidFieldsQuery {
     __type(name: "Droid") {
     namefields {
     nametype {
     namekind }
     }
     }
    }

    然后我们回来:

    {
     "__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 看起来有点奇怪,它没有用于类型的NAME。 那是因为它是"包装器"类型的NON_NULL。 如果我们在这类字段上查询 ofType,我们会找到 String 类型,告诉我们这是一个非空字符串。

    类似地,friendsappearsIn 都没有 NAME,因为它们是 LIST 包装器类型。 我们可以在这些类型上查询 ofType,这将告诉我们这些类型的列表。

    queryIntrospectionDroidWrappedFieldsQuery {
     __type(name: "Droid") {
     namefields {
     nametype {
     namekindofType {
     namekind }
     }
     }
     }
    }

    然后我们回来:

    {
     "__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 }
     }
     ]
     }
    }

    让我们以自省系统的特点为特点,对工具特别有用;让我们向系统提供文档。

    queryIntrospectionDroidDescriptionQuery {
     __type(name: "Droid") {
     namedescription }
    }

    收益率

    {
     "__type": {
     "name": "Droid",
     "description": "A mechanical creature in the Star Wars universe." }
    }

    因此我们可以使用自省访问关于类型系统的文档,并创建文档浏览器,或者丰富的IDE体验。

    这只是触摸了自省系统的表面;我们可以查询 enum 值,类型实现的接口,以及更多。 我们甚至可以在自省系统上自省。 在"自省"部分,该规范详细介绍了这个主题,中的自省插件文件包含实现符合规范的GraphQL查询自省系统代码。

    附加内容

    本自述介绍了 GraphQL.js 引用类型。查询执行。验证和内省系统。 这两种方法的功能包括:执行查询和执行查询,如何格式化响应,如何格式化类型系统以及如何格式化类型系统,以及如何设置GraphQL响应的格式,以及如何设置格式。


    服务  LAN  LANG  EXE  EXEC  Backend  
    相关文章