[译]GraphQL如何把查询转换为响应(How GraphQL turns a query into a response)

1,627 阅读9分钟

原文链接: How GraphQL turns a query into a response

在这篇文章中,我将回答一个简单的问题,GraphQL如何把查询转换为响应?

如果你对GraphQL还不熟悉,那么在阅读之前,先了解一下“How do I GraphQL?”的三分钟介绍。这样你就能从这篇文章中得到更多。

我们这篇文章中将会介绍以下内容:

  • GraphQL queries - 查询
  • Schema and resolve functions - 模式和解析函数
  • GraphQL execution — step by step - 逐步执行

准备好了吗?让我们开始吧!

GraphQL queries

GraphQL查询结构非常简单,易于理解。请看下面的例子:

{
  subscribers(publication: "apollo-stack"){
    name
    email
  }
}

如果我们为Building Apollo构建了一个API,显而易见这个查询将会返回所有订阅了“apollo-stack”订阅者的nameemail。以下是响应的样子:

{
  subscribers: [
    { name: "Jane Doe", email: "jane@doe.com" },
    { name: "John Doe", email: "john@doe.com" },
    ...
  ]
}

注意响应的结构与查询的结构几乎相同。GraphQL的客户端非常简单,它实际上是自解释的!

但是服务端呢?会更复杂吗?

事实证明,GraphQL服务端也相当简单。在阅读完这篇文章之后,您将清楚地了解GraphQL服务器内部发生了什么,并准备好构建自己的服务器。

Schema and Resolve Functions

每个GraphQL服务器都有两个核心部分来决定它的工作方式:schema(模式)resolve functions(解析函数)

模式:模式是可以通过GraphQL服务器获取的数据模型。它定义了允许客户端进行哪些查询,可以从服务器获取什么类型的数据,以及这些类型之间的关系。例如:

schema
具有三种类型的简单GraphQL模式:Author、POST和Query

在GraphQL模式语法中,如下所示:

type Author {
  id: Int
  name: String
  posts: [Post]
}
type Post {
  id: Int
  title: String
  text: String
  author: Author
}
type Query {
  getAuthor(id: Int): Author
  getPostsByTitle(titleContains: String): [Post]
}
schema {
  query: Query
}

译者注:在Apollo-Server2.0中,最后一节schema可以不写

这个模式非常简单:它声明应用程序有三种类型: -  AuthorPOSTQuery。每个查询都必须从它的一个字段开始:getAuthorgetPostsByTitle。你可以把它们看作是REST端点,除了更强大之外。

AuthorPost 相互引用。你可以通过 Authorposts字段获取 Post,也可以通过 Postauthor字段从获取 Author

模式告诉服务器允许客户端进行哪些查询,以及不同类型之间的关系,但是其中有一个关键信息是不包含的:每种类型的数据来自哪里!

这就是解析函数的用途。

Resolve Functions

解析功能有点像路由。它们指定模式中的类型和字段如何连接到各种后端,解决“如何为 Author 获取数据?”和“我需要用什么参数调用哪个后端才能获得 POST 的数据?”这样的问题。

GraphQL解析函数可以包含任意代码,这意味着GraphQL服务器可以与任何类型的后端,甚至其他GraphQL服务器对话。例如,Author 类型可以存储在SQL数据库中,而 POST 可以存储在MongoDB中,甚至可以由微服务处理。

也许GraphQL最大的特点是它对客户端隐藏了所有后端复杂性。不管您的应用程序使用了多少后端,客户端只会看到一个带有应用程序简单的、自文档化API的GraphQL端点。

下面是两个解析函数的例子:

getAuthor(_, args){
  return sql.raw('SELECT * FROM authors WHERE id = %s', args.id);
}
posts(author){
  return request(`https://api.blog.io/by_author/${author.id}`);
}

当然,您不会将查询或url直接写入一个解析函数中,而是将其放在一个单独的模块中。但你已经明白了解析函数的使用。

Query execution — step by step

好了,现在您已经了解了模式和解析函数,让我们来看看实际查询的执行情况。

附带说明:下面的代码是GraphQL-JS的代码,它是GraphQL的JavaScript参考实现,但是在我所知道的所有GraphQL服务器中,执行模型是相同的。

在本节的末尾,您将了解GraphQL服务器如何使用模式和解析函数一起执行查询并生成所需的结果。

下面是一个与前面介绍的模式对应的查询。它获取一个作者的姓名、该作者的所有帖子以及每个帖子的作者的姓名。

{
  getAuthor(id: 5){
    name
    posts {
      title
      author {
        name # this will be the same as the name above
      }
    }
  }
}

附带说明:如果仔细观察,您会注意到这个查询两次获取同一个作者的名称。我在这里这样做只是为了说明GraphQL,同时保持模式尽可能简单。

以下是服务器响应查询的三个关键步骤:

1、解析

2、验证

3、执行

Step 1: 解析查询

首先,服务器解析字符串并将其转换为AST(抽象语法树)。如果有任何语法错误,服务器将停止执行并将语法错误返回给客户端。

Step 2: 验证

一个查询在语法上可以是正确的,但仍然没有任何意义,就像下面的英语句子在语法上是正确的,但是没有任何意义:“The sand left through the idea”。

验证阶段确保在开始执行之前给定模式查询是有效的。它检查如下:

  • getAuthor 是查询类型的字段吗?
  • getAuthor 是否接受名为id的参数?
  • getAuthor 返回的类型上是否有nameposts字段?
  • ...诸如此类

作为一个应用程序开发人员,您不需要担心这个部分,因为GraphQL服务器会自动完成。这与大多数RESTfulAPI形成了对比,在这种情况下,需要由开发人员来确保所有参数都是有效的。

Step 3: 执行

如果通过验证,GraphQL服务器将执行查询。

每个GraphQL查询都具有树的形状,也就是说,它从不是循环的。执行从Query的根开始。首先,执行器调用顶层字段的解析函数-在本例中,只是 getAuthor。它等待直到所有这些解析函数返回一个值,然后以级联的方式在下一级继续。如果一个解析函数返回一个promise,执行者将等待该promiseresolved。

这是对执行流的一段描述。我认为,当以不同的方式展示事物时,它们总是更容易理解,所以我制作了一张图表,一张表格,甚至一段视频,一步步地带你去看。

图形式的执行流程:

exexution
执行从最上面开始。在同一级别上的解析函数是并发执行的

表形式的执行流程:

3.1: run Query.getAuthor
3.2: run Author.name and Author.posts (for Author returned in 3.1)
3.3: run Post.title and Post.author (for each Post returned in 3.2)
3.4: run Author.name (for each Author returned in 3.3)

为了方便起见,这还是上面的查询:

{
  getAuthor(id: 5){
    name
    posts {
      title
      author {
        name # this will be the same as the name above
      }
    }
  }
}

在这个查询中,只有一个根字段 getAuthor 和一个值为5的参数idgetAuthor 解析函数将执行并返回Promise。

getAuthor(_, { id }){
  return DB.Authors.findOne(id);
}
// let's assume this returns a promise that then resolves to the
// following object from the database: 
{ id: 5, name: "John Doe" }

当数据库调用返回时,Promise将被resolved。一旦发生这种情况,GraphQL服务器将获取此解析函数的返回值-在本例中为一个对象-并将其传递给Author上nameposts字段的解析函数,因为这些字段是查询中请求的字段。nameposts字段的解析函数并行运行。

name(author){
  return author.name;
}
posts(author){
  return DB.Posts.getByAuthorId(author.id);
}

name解析函数非常简单:它只返回刚刚从 getAuthor 解析函数传递下来的Author对象的name属性。

posts解析函数调用数据库并返回POST对象列表:

// list returned by DB.Posts.getByAuthorId(5)
[{
  id: 1,
  title: "Hello World",
  text: "I am here",
  author_id: 5
},{
  id: 2,
  title: "Why am I still up at midnight writing this post?",
  text: "GraphQL's query language is incredibly easy to ...",
  author_id: 5
}]

注意: GraphQL-JS等待列表中所有的Promise被resolved或者rejected之后才执行下一级的解析函数

因为查询请求了每个帖子的titleauthor字段,所以GraphQL并行运行四个解析函数:每个帖子的titleauthor

title解析函数像name一样是微不足道的,author解析函数与 getAuthor 的函数相同,只不过它在POST上使用author_id字段,而 getAuthor 函数使用id参数:

author(post){
  return DB.Authors.findOne(post.author_id);
}

最后,GraphQL执行器再一次调用Author的name解析函数,这一次使用POSTS的author解析函数返回的Author对象。它执行了两次—— 每个帖子执行一次。

到这里执行部分已经结束了!剩下要做的就是将结果传递到查询的根目录,并返回结果:

{
  data: {
    getAuthor: {
      name: "John Doe",
      posts: [
        {
          title: "Hello World",
          author: {
            name: "John Doe"
          }
        },{
          title: "Why am I still up at midnight writing this post?",
          author: {
            name: "John Doe"
          }
        }
      ]
    }
  }
}

注意:这个例子稍微简化了一些。真正的生产GraphQL服务器将使用批处理和缓存来减少对后端的请求数量,并避免产生冗余的请求,比如获取同一作者两次。但这是另一篇文章的主题!

结语

如你所见,一旦你深入到它,GraphQL是非常容易理解的!我认为GraphQL在解决诸如联表、过滤、参数验证、文档等传统RESTfulAPI中很难解决的问题上,是非常出色的。

当然,GraphQL比我在这里写的要多得多,但这是以后文章的主题!

如果这让您对自己尝试GraphQL感兴趣,您应该查看我们的GraphQL server tutorial,或者阅读有关 using GraphQL on the client together with React + Redux.的相关内容。

2018年更新:理解使用Apollo Engine执行GraphQL

自从Jonas撰写这篇文章以来,我们还构建了一个名为Apollo Engine的服务,通过提供以下功能帮助开发人员了解和监视其GraphQL服务器中发生的事情:

如果您有兴趣看到您的GraphQL查询在实际应用中的执行,您可以在这里登录并检测您的服务器。如果您有兴趣支持使用GraphQL运行高性能的现代应用程序,我们可以帮助您!让我们知道

Apollo Engine
Apollo Engine的运行概况:查询服务时间和请求率/错误率图表的热力图。