GraphQL服务器的结构和实现(第一部分)
英文原址 该序列文章其实我觉得只有第三篇有些用处前两篇、内容相对比较尴尬(这第一篇只是顺手翻译过来)。如果你比较赶时间或者初学者其实本篇我觉得不太适合,感兴趣可以直接看第二(英文)和第三(中文)篇
在开始使用GraphQL时,首先要问的问题之一是如何构建GraphQL服务器?facebook
已经已经简单地发布了一个GraphQL规范,GraphQL服务器理论上可以使用任何您喜欢的编程语言实现。
在开始构建服务器之前,GraphQL要求您设计一个 Schema
,该Schema
定义服务器的的API。在这篇文章中,我们希望了解Schema
的主要组件,阐明实际实现它的机制,并了解如何使用GraphQL.js这样的库,graphql-tools
与graphene-js
在此过程中为您提供帮助。
本文仅涉及简单的GraphQL功能 - 没有定义服务器如何与客户端通信的网络层概念。重点是“GraphQL执行引擎”和查询解析过程的内部工作原理。要了解网络层,请查看下一篇文章。
GraphQL Schema 定义服务器的API
Defining schemas:模式定义语言
GraphQL有自己的类型语言, GraphQL Schema:Schema Definition Language
(SDL)。在最简单的形式中,GraphQL SDL可用于定义如下所示的类型:
type User {
id: ID!
name: String
}
User
单独的类型不会向客户端应用程序公开任何功能,它只是在应用程序中定义用户模型的结构。为了增加功能的API,你需要字段添加到根类型的GraphQL架构:Query
,Mutation
和Subscription
。这些类型定义了GraphQL API 的入口点。
例如,请考虑以下查询:
query {
user(id: "abc") {
id
name
}
}
仅当相应的GraphQL架构Query
使用以下user
字段定义根类型时,此查询才有效:
type Query {
user(id: ID!): User
}
因此,Schema
的根类型决定了服务器可以接受的Query
,Mutation
的形状。
GraphQL架构为客户端 - 服务器通信提供了明确的契约。
该GraphQL Schema
对象是GraphQL服务器的核心
GraphQL.js是Facebook的GraphQL参考实现,为其他库提供了基础,比如graphql-tools
和graphene-js
。使用这些库中的任何一个时,您的开发过程都以一个GraphQL Schema
对象为中心,包括两个主要组件:
-
Schema
定义 - 解析器函数形式实现
对于上面的示例,该GraphQLSchema
对象如下所示:
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString }
}
})
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: {
id: { type: GraphQLID }
}
}
}
})
})
如您所见,Schema
的SDL版本可以直接转换为类型的JavaScript表示形式GraphQL Schema
。请注意,此Schema
没有任何解析器 - 因此不允许您实际执行任何Query
,Mutation
。更多关于下一节的内容。
解析器实现API
GraphQL服务器中的结构与行为
GraphQL具有明确的结构 和行为分离。GraphQL服务器的结构 - 正如我们刚刚讨论的那样 - 是它的Schema
,是服务器功能的抽象描述。这个结构通过一个确定服务器行为的具体实现来实现。实现的关键组件是所谓的解析器功能。
GraphQL架构中的每个字段都由解析程序支持。
在最基本的形式中,GraphQL服务器在其模式中的每个字段都有一个解析器函数。每个解析器都知道如何获取其字段的数据。由于GraphQL查询本质上只是一个字段集合,因此为了收集所请求的数据,实际需要做的所有GraphQL服务器都会调用查询中指定的字段的所有解析器函数。(这也是为什么GraphQL经常与RPC风格的系统进行比较的原因,因为它本质上是一种用于调用远程函数的语言。)
解析器解析器功能
使用GraphQL.js时,GraphQL Schema
对象中某个类型的每个字段都可以resolve
附加一个函数。让我们从上面考虑我们的例子,特别user
是Query
类型上的字段- 这里我们可以添加一个简单的resolve
函数如下:
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: {
id: { type: GraphQLID }
},
resolve: (root, args, context, info) => {
const { id } = args // the `id` argument for this field is declared above
return fetchUserById(id) // hit the database
}
}
}
})
})
假设一个函数fetchUserById
实际可用并返回一个User
实例(带有id
和name
字段的JS对象),该resolve
函数现在可以执行该Schema
。
在我们深入研究之前,让我们花一点时间来理解传递给解析器的四个参数:
-
root
(有时也称为parent
):还记得我们如何说一个GraphQL服务器需要做的就是解析一个查询是调用查询字段的解析器吗?好吧,它正在做广度优先(逐级)并且root
每个解析器调用中的参数只是前一次调用的结果(如果没有另外指定则初始值为null
)。 -
args
:该参数携带用于查询的参数,在这种情况下id
的User
要提取。 -
context
:一个通过解析器链传递的对象,每个解析器可以写入和读取(基本上是解析器通信和共享信息的方法)。 -
info
:查询或变异的AST表示。您可以在本系列的第III部分中阅读有关详细信息的更多信息:揭开GraphQL解析器中的信息参数的神秘面纱。
之前我们曾声明GraphQL架构中的每个字段都由解析器函数支持。现在我们只有一个解析器,而我们的模式总共有三个字段:Query
类型上的根字段用户,以及类型上的id
和name
字段User
。其余两个领域仍然需要他们的解析器。正如您将看到的,这些解析器的实现是微不足道的:
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: {
type: GraphQLID,
resolve: (root, args, context, info) => {
return root.id
}
},
name: {
type: GraphQLString,
resolve: (root, args, context, info) => {
return root.name
}
}
}
})
查询执行
注:
根(域)
指Query,Mutation和Subscription
考虑到我们上面的查询,让我们了解它是如何执行的以及如何收集数据。在总查询中包含三个字段:user
(在根字段),id
和name
。这意味着当查询到达服务器时,服务器需要调用三个解析器函数 - 每个字段一个。让我们来看看执行流程:
- 查询到达服务器。
- 服务器为根域调用解析器
user
- 让我们假设fetchUserById
返回此对象:{ "id": "abc", "name": "Sarah" }
- 服务器为类型
id
上的字段调用解析程序User
。root
此解析程序的输入参数是前一次调用的返回值,因此它可以简单地返回root.id
。 - 类似于3,但最后返回
root.name
。(注意3和4可以并行发生。) - 解决过程终止 - 最后结果包含一个
data
字段以符合GraphQL规范:
{
"data": {
"user": {
"id": "abc",
"name": "Sarah"
}
}
}
现在,你真的需要写解析器为user.id
和user.name
自己呢?使用GraphQL.js时,如果实现与示例中一样简单,则不必实现解析程序。因此,您可以省略它们的实现,因为GraphQL.js已经根据字段名称和根参数推断出它需要返回的内容。
优化请求:DataLoader模式
使用上述执行方法,当客户端发送深层嵌套查询时,很容易遇到性能问题。假设我们的API也有包含要求和允许此查询的注释的文章:
query {
user(id: "abc") {
name
article(title: "GraphQL is great") {
comments {
text
writtenBy {
name
}
}
}
}
}
请注意,我们如何要求特定article
的给予user
,以及其comments
与name
谁写它们的用户号第
我们假设这篇文章有五条评论,都是由同一个用户编写的。这意味着我们会击中writtenBy
解析器五次,但每次都会返回相同的数据。该的DataLoader让你在这些种情况,以避免N + 1查询问题优化-总的想法是,解析器调用批处理,因此数据库(或其他数据源)只需要进行一次打击。
要了解有关DataLoader的更多信息,您可以观看Lee Byron 撰写的精彩视频:DataLoader - 源代码演练(约35分钟)
GraphQL.js vs. graphql-tools
现在让我们来谈谈可用的库,它们可以帮助您在JavaScript中实现GraphQL服务器 - 这主要是关于GraphQL.js和graphql-tools
。之间的区别。
GraphQL.js为graphql-tools提供了基础
要理解的第一个关键事项是GraphQL.js提供了基础graphql-tools
。它通过定义所需类型,实现模式构建以及查询验证和解决方案来完成所有繁重任务。graphql-tools
然后在GraphQL.js之上提供一个瘦的便利层。
让我们快速浏览一下GraphQL.js提供的功能。请注意,其功能通常以GraphQLSchema
:
-
parse
并且buildASTSchema
:给定在GraphQL SDL中定义为字符串的GraphQL架构,这两个函数将创建一个GraphQLSchema实例:const schema = buildASTSchema(parse(sdlString))
。 -
validate
:给定GraphQLSchema
实例和查询,validate
确保查询遵循模式定义的API。 -
execute
:给定GraphQLSchema
实例和查询,execute
调用查询字段的解析器并根据GraphQL规范创建响应。当然,这只有在解析器是GraphQLSchema
实例的一部分时才有效(否则它只是带有菜单但没有厨房的餐厅)。 -
printSchema
:获取一个GraphQLSchema
实例并在SDL中返回其定义(作为字符串)。
请注意,GraphQL.js中最重要的功能是graphql
获取GraphQLSchema
实例和查询 - 然后调用validate
和execute
:
graphql(schema, query).then(result => console.log(result))
要了解所有这些函数,请查看这个简单的节点脚本,该脚本在一个简单的示例中使用它们。
该graphql
函数正在对一个模式执行GraphQL查询,该模式本身已经包含结构和行为。因此,主要作用graphql
是协调解析器函数的调用,并根据提供的查询的形状打包响应数据。在这方面,该功能实现的graphql
功能也称为GraphQL引擎。
graphql-tools
:桥接接口和实现
使用GraphQL的一个好处是,您可以采用模式优先开发过程,这意味着您构建的每个功能首先在GraphQL模式中显示 - 然后通过相应的解析器实现。这种方法有许多好处,例如它允许前端开发人员在由后端开发人员实际实现之前开始对抗模拟的API - 感谢SDL。
GraphQL.js的最大缺点是它不允许您在SDL中编写模式,然后轻松生成a的可执行版本
GraphQLSchema
。
如上所述,您可以GraphQLSchema
使用parse
和从SDL 创建实例buildASTSchema
,但这缺少了resolve
使执行成为可能的必需功能!GraphQLSchema
使用GraphQL.js 创建可执行文件的唯一方法是手动将resolve
函数添加到模式的字段中。
graphql-tools
用一个重要的功能填补这个空白:addResolveFunctionsToSchema
。这非常有用,因为它可用于提供更好的基于SDL的API来创建模式。而这恰恰graphql-tools
与以下内容有关makeExecutableSchema
:
const { makeExecutableSchema } = require('graphql-tools')
const typeDefs = `
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String
}`
const resolvers = {
Query: {
user: (root, args, context, info) => {
return fetchUserById(args.id)
}
},
}
const schema = makeExecutableSchema({
typeDefs,
resolvers
})
因此,使用graphql-tools
它的最大好处是它可以很好地连接声明性模式和解析器!
什么时候不用graphql-tools
?
我们刚刚了解到,graphql-tools
它的核心在GraphQL.js之上提供了一个便利层,那么在它不是实现服务器的正确选择时会出现这种情况吗?
与大多数抽象一样,graphql-tools
通过牺牲其他地方的灵活性,可以使某些工作流更容易。它提供了一个惊人的“入门” - 经验,并避免摩擦,快速建立一个GraphQLSchema
。如果你的后端有更多的自定义要求,比如动态构建和修改你的模式,它的紧身胸衣可能有点太紧 - 在这种情况下你可以回到使用GraphQL.js。
快速说明 graphene-js
graphene-js
是一个新的GraphQL库,遵循Python对应的想法。它还使用了GraphQL.js,但不允许SDL中的模式声明。
graphene-js
深深地接受现代JavaScript语法,提供直观的API,其中查询和突变可以作为JavaScript类实现。看到更多的GraphQL实现以新鲜的想法丰富生态系统,这是非常令人兴奋的!
结论
在本文中,我们揭示了GraphQL执行引擎的机制和内部工作原理。从GraphQL模式开始,该模式定义服务器的API并确定将接受哪些查询和突变,以及响应格式的外观。然后,我们深入研究了解析器函数,并概述了GraphQL引擎在解析传入查询时启用的执行模型。最后概述了可用于帮助您实现GraphQL服务器的JavaScript库。
如果您想要实际了解本文中讨论的内容,请查看此存储库。请注意,它有一个
graphql-js
和graphql-tools
分支来比较不同的方法。
通常,重要的是要注意GraphQL.js提供了构建GraphQL服务器所需的所有功能 - graphql-tools
只需在顶部实现一个便利层,以满足大多数用例并提供一个很棒的“入门”体验。只有在构建GraphQL架构的更高级要求时,才可以关闭手套并使用简单的GraphQL.js。
在接下来的文章中,我们将讨论实施GraphQL服务器,比如网络层和不同的库express-graphql, apollo-server
和graphql-yoga
。然后,第3部分介绍了GraphQL解析器中info对象的结构和作用。