[译] GraphQL - 学习 - 执行

 阅读约 9 分钟

原文地址: Execution

在被验证之后,GraphQL 查询将由 GraphQL 服务器执行,该服务器返回的结果反映了请求查询的形状,通常是JSON。

GraphQL 不能在没有类型系统的情况下执行查询,让我们使用一个示例类型系统来演示执行查询。这是在这些文章的例子中使用的相同类型系统的一部分:

type Query {
  human(id: ID!): Human
}

type Human {
  name: String
  appearsIn: [Episode]
  starships: [Starship]
}

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

type Starship {
  name: String
}

为了描述在执行查询时发生的情况,我们使用一个例子来说明一下

{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}

结果

{
  "data": {
    "human": {
      "name": "Han Solo",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "starships": [
        {
          "name": "Millenium Falcon"
        },
        {
          "name": "Imperial shuttle"
        }
      ]
    }
  }
}

您可以将 GraphQL 查询中的每个字段视为前面类型的函数或方法,返回下一个类型。事实上,这正是GraphQL 的工作方式。每个类型的字段都由一个名为 resolver 的函数来支持,该函数由 graphQL 服务器开发人员提供。当一个字段被执行时,将调用相应的解析器来生成下一个值

如果一个字段产生一个标量值,比如字符串或数字,那么执行就完成了。但是,如果一个字段产生一个对象值,那么查询将包含另一个应用于该对象的字段。这将一直持续到标量值到达为止。GraphQL 查询总是以标量值结束。

根字段 & 解析器 / Root fields & resolvers

在每个 GraphQL 服务器的顶层,都是一个类型,它表示所有可能的入口到 GraphQL API 中,它通常被称为根类型或查询类型。

在本例中,我们的查询类型提供了一个名为 human 的字段,该字段接受参数 id。该字段的解析器函数很可能访问一个数据库,然后构造并返回一个人工对象。

Query: {
  human(obj, args, context) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}

这个例子是用 JavaScript 编写的,但是 GraphQL 服务器可以用很多不同的语言来编写。解析器函数接收三个参数

  • obj 以前的对象,对于根查询类型的字段,通常不使用。
  • args 在 GraphQL 查询中提供给该字段的参数
  • context 一个为每个解析器提供的值,并保存重要的上下文信息,比如当前登录的用户,或者访问数据库。

匿名解析器 / Asynchronous resolvers

让我们仔细看看这个解析器函数中发生了什么。

human(obj, args, context) {
  return context.db.loadHumanByID(args.id).then(
    userData => new Human(userData)
  )
}

context 用于提供对数据库的访问,该数据库用于通过在 GraphQL 查询中作为参数提供的 id 来为用户加载数据。因为从数据库中加载是异步操作,所以这返回了一个 Promise。在JavaScript 中,Promise 用于处理异步值,但在许多语言中都存在相同的概念,通常称为Futures, Tasks or Deferred。当数据库返回时,我们可以构造并返回一个新的 Human 对象。

注意,虽然解析器函数需要知道 Promise ,但是 GraphQL 查询没有。它只是希望 human 的字段能够返回一些东西,然后再问它的 name 。在执行期间,GraphQL 将等待 Promise, Future, 和 Tasks 在继续之前完成,并在最佳并发性的情况下完成

简单的解析器 / Trivial resolvers

既然已经有了一个 Human 对象,那么 GraphQL 执行就可以继续使用它所请求的字段

Human: {
  name(obj, args, context) {
    return obj.name
  }
}

GraphQL 服务器由一个类型系统驱动,该系统用于确定下一步要做什么。甚至在人类 Human 返回任何东西之前,GraphQL 知道下一步将是解析 Human 类型的字段,因为类型系统告诉它,Human 字段将返回一个人。

在这种情况下,解析名称非常简单。名称解析器函数被调用,obj 参数是来自前一个字段的 new Human 对象。在这种情况下,我们期望 Human 对象拥有一个可以直接读取和返回的 name 属性。

事实上,许多 GraphQL 库会让你忽略这个简单的解析器,并假设如果一个解析器没有为字段提供,那么应该读取和返回相同名称的属性。

标量强制 / Scalar coercion

在解析 name 字段时,可以同时解析 appearsInstarships 字段。appearsIn 也可以有一个简单的解析器,如下:

Human: {
  appearsIn(obj) {
    return obj.appearsIn // returns [ 4, 5, 6 ]
  }
}

注意,我们的类型系统声明将返回带有已知值的枚举值,但是这个函数返回的是数字!事实上,如果我们查看结果,就会看到适当的枚举值被返回。这是怎么运行的呢

这是标量强制的一个例子。类型系统知道预期输出的内容,并将解析器函数返回的值转换为维护 API 约定的值。在本例中,可能在我们的服务器上定义了一个枚举类型,该枚举在内部使用4、5和6等数字,但在 GraphQL 类型系统中表示为枚举值。

列表解析器 / List resolvers

我们已经看到,当一个字段返回了上面出现的 appearsIn 的列表时,会发生什么事情。它返回一个枚举值列表,因为这是类型系统所期望的,所以列表中的每个项都被强制到适当的 enum 值。当 starships 字段获取的时候会发生什么?

Human: {
  starships(obj, args, context) {
    return obj.starshipIDs.map(
      id => context.db.loadStarshipByID(id).then(
        shipData => new Starship(shipData)
      )
    )
  }
}

这个字段的解析器不仅仅是返回一个 Promise,它还在返回一个 Promise 列表。Human 对象有一列他们所引导的 Starships 的 id 列表,但是我们需要加载所有这些 id 来获取真正的 Starship 对象。

GraphQL 将会在继续之前等待所有这些 Promise,当留下一个对象列表时,它将并发地继续加载每个条目的 name 字段。

组合结果 / Producing the result

每个字段是解析完成后, 由此产生的值被存入一个 key-value 映射的字段名(或别名)作为键和解析值作为值, 继续从底部叶子字段查询备份到根元素的原始字段的查询类型。这些共同生成的结构反映了最初的查询,然后可以将其发送(通常是JSON)到请求它的客户端。

让我们最后看看原始查询,看看这些解析函数是如何产生结果的:

{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}

查询结果

{
  "data": {
    "human": {
      "name": "Han Solo",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "starships": [
        {
          "name": "Millenium Falcon"
        },
        {
          "name": "Imperial shuttle"
        }
      ]
    }
  }
}
阅读 706更新于 2017-12-27

推荐阅读
酸柠檬
用户专栏

柠檬是苦的. 酸柠檬更不知道是什么味道这就是程序的味道个中滋味, 无法对外人道深思回味, 终得豁然

17 人关注
44 篇文章
专栏主页
目录