近年来,GraphQL实际上已经作为一种模式/库/类型系统而迅速普及。它提供了REST无法提供的许多功能,它的标准化和灵活性确实为REST的采用提供了帮助。当我开始寻找有关graphql内部工作方式的详细说明时,我很难找到任何不太高级的东西。我想看看模式,查询,类型和解析器如何机械地协同工作。
本文不可能全神贯注于所有部分,因此我将重点放在查询生命周期的执行部分。
有关此演讲的视频可以在这里找到。我的“内幕”的一部分系列在这里。
类型系统-类型定义定义数据的外观。编写它们是为了显示其中包含的内容,并突出关系(事物之间的关系)。该系统定义语言可以转换为AST。
上面的第(3)点引用了官方规范,该规范定义了类型,验证和执行模式的规则。可以在以下位置找到:
所有语言都遵循规范,并且JS graphql库经常引用其中的一部分。我们将在本节中介绍该库。
建立架构对于graphql应用程序来说是重要的部分,如上所述,它定义了所有类型及其关系。有两个步骤
如果它不是GraphQL模式,则解析器将引发错误。请参阅来自types / schema.js的代码段(如下)
我们需要一个架构,它是GraphQLSchema的类型实例(请参见上面的代码段)。然后,我们需要在模式内部匹配类型的对象,例如标量或对象。
const OddType = new({名称:" Odd",序列化(值){if(value%2 ==== 1){返回值}},})
为GraphQL定义的本机标量类型为ID,Int,Float,String和Boolean。您可以在类型系统中定义自己的类型。
const Book = new({name:' Book',fields:()=>({id:{type:new(GraphQLID)},title:{type:new},author:{type:新(作者)},})}
您定义的几乎所有GraphQL类型都是对象类型。对象类型有一个名称,但最重要的是描述它们的字段。
GraphQLSchema {astNode:{kind:' SchemaDefinition&#39 ;, ...},extensionASTNodes:[],_queryType:Query,... _typeMap:{Query:Query,ID:ID,User:User,String:字符串,...},...}
它也保存AST信息,但根查询和类型位于_typeMap属性下。 解析器不能包含在GraphQL模式语言中,因此必须单独添加它们。 它们被添加到_typeMap。< Type> ._ fields。< field>下的_typeMap属性中,因此添加解析器后,架构对象可能如下所示: _typeMap:{查询:{_fields:{用户:{解析:[功能]}}},用户:{_fields:{地址:{解析:[功能]}}}}} 从HTTP请求的角度来看,社区已在HTTP POST方法上进行了很大的标准化。 但是,当服务器收到查询时会发生什么? GraphQL规范概述了所谓的"请求生命周期"。 这详细说明了当请求到达服务器以产生结果时发生的情况。 GraphQL的Lexer识别GraphQL查询的片段(单词/标记)并为每个单词赋予含义
{" kind" :"文档" ,"定义" :[{" kind" :" OperationDefinition" ,"操作" :" query" ,"名称" :{" kind" :"名称" ,"值" :"首页" }," selectionSet" :{" kind" :" SelectionSet" ,"选择" :[{" kind" :"字段" ,"名称" :{" kind" :"名称" ,"值" :"帖子" }," selectionSet" :{" kind" :" SelectionSet" ,"选择" :[{" kind" :"字段" ,"名称" :{" kind" :"名称" ,"值" :"标题" }},{" kind" :"字段" ,"名称" :{" kind" :"名称" ,"值" :"作者" }}]}}]}}]}
此步骤确保可以根据提供的模式执行请求。在graphql-js库的validate.js下找到。
尽管通常在执行之前就运行它,但单独运行可能会很有用。例如,由客户端在将查询发送到服务器之前。好处是,验证器可以在将无效查询发送到服务器之前对其进行标记,从而保存HTTP请求。
它通过对照模式对象中其对应的类型定义检查查询AST文档中的每个字段来工作。它将比较参数类型的兼容性以及强制检查。
这是迄今为止最密集的步骤,也是我经常发现其机制最令人困惑的步骤。我们将在第2部分中对此步骤进行更深入的研究,因此让我们从高层次上看一下所涉及的过程。
识别操作,即查询或变异? (可以一次进行多个查询)
对于第2步,GraphQL遍历选择集中的每个字段,如果它是标量类型,则解析字段(executeField),否则递归选择集,直到解析为标量。
这种工作方式是,引擎立即调用根级别的所有字段,然后等待所有字段返回(包括所有promise)。然后,在读取返回类型后,它会级联到树中,并使用来自父解析器的数据调用所有子字段解析器。然后,对那些字段返回类型重复此级联。简单地说,它首先调用顶级查询,然后调用每种类型的根解析器。
该机制进行了标量强制转换。在这里发挥作用。解析器返回的任何值都将被转换(基于返回类型),以维护API合同。例如,解析器返回字符串" 123"附加了数字类型的数字将返回Number(" 123")(即数字)。
值得一提的是内省系统。这是GraphQL API架构使用的一种机制,允许客户端了解支持哪些类型和操作以及采用哪种格式。
客户端可以查询GraphQL API中的__schema字段,该字段始终在Query的根类型上可用。任何交互式GraphQL UI都依赖于向服务器发送IntrospectionQuery请求,该请求将生成文档并自动完成。
作为研究的一部分,我涵盖了许多不同的库,因此我认为有必要快速概述JS生态系统中的主要库。
GraphQL规范的参考实现,但也包含用于构建GraphQL服务器,客户端和工具的有用工具,它是一个拥有许多单一存储库的GitHub组织。
需要特定于库的GraphQLSchema实例。为了成为可执行模式,它需要解析器功能。
将架构解析为AST之后,它将转换为GraphQLSchema类型
它是对graphql-js的抽象。具有许多功能,包括生成完全受规范支持的架构以及将多个架构拼接在一起。
将此指向源以从中加载您的架构,它返回GraphQLSchema。
内省默认情况下在生产上是禁用的,但是在非生产中,它会使用内省来公开/ graphql游乐场。
插入apollo服务器并实时提供GraphQL服务器的统计信息。如:
在这里,我们将构建自己的GraphQL执行函数,解析的查询和我们的架构都可以运行该函数。它将忽略任何验证。我们将使用GraphQL实例类型创建模式,而忽略模式解析步骤。
我们在查询生命周期部分的前面已经看到,graphql-js库包含必要的函数,可以将它们单独导入以供使用。例如parser(),validate()和execute()。除了这些功能之外,它还导出类型实例,这些实例可用于构建graphql模式。虽然我们将不会使用它们提供的任何功能,但会使用它们提供的类型实例来构建有效的架构。
您定义的大多数GraphQL类型将是对象类型,它们具有名称和描述字段 例如,在类型{a}上的字段引用(例如a),片段扩展(例如... c和内联片段扩展...) 包括当前正在执行的类型系统的模式以及查询文档中定义的片段 第1步是使用graphql-js收集我们的查询AST和架构实例,以便可以进行处理 这些是我们将从graphql-js库导入的类型。 2个标量,1个对象和模式类型。 该查询将如下所示,它与普通的graphql查询的运行方式非常接近,除了通常在执行之前还有一个额外的parse(query)步骤。 //" document" =是我们的查询(采用硬编码的AST形式const schema = new({…< schema objects>}))// GraphQLSchema包含根级查询,字段,类型,解析器的类实例ourExecute({schema,document}) ; //使用架构执行查询AST
//模式=根据" schema.graphql"构建和" graphql-tool" loadSchemaSync / makeExecutableSchema / addResolversToSchema const document = parse(query)execute({schema,document})执行
模式-这是模式的外观(请参阅架构前解析表单的注释)
现在,我们已经完成了3种情况的设置,我们将寻求构建实际的执行功能。代码在下面,将进行解释。
从第54行开始。在这里,我们用第一个定义以及类型映射调用execute。因此,我们将POC限制为我们知道的第一个操作是查询。实际上,它将迭代定义。
在execute(第3行)内部,获取Query和rootTypes(第6行),然后处理选择集中的第一个选择(第8行)。再次对此POC进行硬编码。然后,我们获取returnType(第10行),该值将用于针对查询结果进行验证,在此省略了。第11行创建一个空的操作对象,以存储任何操作参数以供以后使用。
在第13行,我们检查参数并获取参数名称和值。我们还可以获取用于验证的类型(第16行)。
我们在第19行定义了executeField,接下来将继续该过程。最后,我们使用当前选择集和一个空对象调用executeFields。这将构成我们对退货的回应。
现在执行executeFields。它以选择集,响应对象和字段类型为参数。这样一来,它就可以递归调用查询树,并在我们进行操作时构建响应数据。
第20行我们遍历所有选择集选择(因此,这部分适用于多个选择)。我们抓取该字段(第21行)并在根查询级别(第24行)进行检查。对于"场景1"该字段是用户。因此,它将检查Query._fields [user] .resolve,它在"场景1"中确实存在。
如果存在(即"场景1"),我们用参数名称和值调用解析器,然后将解析器返回的数据放到响应中的该字段名称(例如,用户)上(第28行) 。
然后,在第32行中,检查Schemas rootTypes对象上是否存在返回类型字段。如果是这样,我们将使用父解析器数据执行它,然后将其写入响应中的字段(第36行)。对于"场景1" returnType是一个String,作为标量,它没有_fields,因此不会触发。对于"场景2" returnType是一个具有_fields的Person,其中一个是name,但现在该字段是test,因此直到下面再次出现时才触发。
在第40行和第41行,我们检查此选择本身是否具有选择集,是否执行选择集。我们处理选择集,该字段的当前响应数据以及当前字段名称。因此,我们将使用更深的选择集递归调用executeFields,并建立单个响应以返回。 "情况2"将首先调用根查询解析器,然后使用内部名称字段再次调用executeFields,在其中将找到字段解析器并使用现有的解析器数据进行调用。
值得注意的是,我们省略了任何标量检查,使用真正的graphql-js.execute会检查字段类型作为验证的一部分,以确保非标量应具有子选择。但是,第32行(rootTypes?。[returnType]?._ fields?。[field] ?. resolve)是一种标量检查(标量没有_fields)。
为了检查它是否起作用,我编写了一些在此处找到的单元测试。它包括一些纯断言和间谍,我最初将它们针对graphql-js.execute()进行运行以确保它们被正确编写。然后,我换用了我的执行器。模式对象是前面显示的对象,但是所有这些都在一起。
我鼓励任何有兴趣的人签出代码并亲自使用该机制。 如前所述,真正的graphql执行程序还有很多其他部分,我们从库中省略了这些部分。 其中一些是: 非常感谢您阅读(或观看),我从这项研究中学到了很多有关GraphQL的信息,希望对您有用。 您可以在此处找到所有这些代码的存储库。