真正的谈话:GraphQL.js 文档不是那么好。在我看来,他们永远不应该使用示例buildSchema
首先,因为它会导致这种混乱,这是可以理解的。
GraphQL.js(即graphql
package) 是 GraphQL 的 JavaScript 实现。在 GraphQL.js 中构建模式是通过构建一个实例以编程方式完成的GraphQLSchema
class:
const userType = new GraphQLObjectType({
name: 'User',
fields: {
id: {
type: GraphQLID,
},
email: {
type: GraphQLString,
},
},
});
const queryType = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: userType,
resolve: () => ({ id: 1, email: '[email protected] /cdn-cgi/l/email-protection' }),
},
},
});
const schema = new GraphQLSchema({
query: queryType,
})
如果我们用模式定义语言(SDL)打印这个模式,它看起来像这样:
type Query {
user: User
}
type User {
id: ID
email: String
}
使用 SDL 比编写所有代码要容易得多。但是,GraphQL.js 不提供从 SDL 构建功能齐全的架构的方法。 It does提供一个buildSchema
函数,但是这个实用程序构建了一个模式没有任何解析器(以及许多其他功能,例如联合/接口类型解析)。
The graphql-tools
包提供了一个makeExecutableSchema
函数允许您从 SDL 和解析器映射对象构建架构。这是在幕后使用的apollo-server
and graphql-yoga
. makeExecutableSchema
使用 SDL 构造模式buildSchema
然后改变结果对象,添加解析器事后 https://github.com/apollographql/graphql-tools/blob/master/src/generate/addResolveFunctionsToSchema.ts.
在 GraphQL.js 中,resolve
字段的函数(或解析器)有四个参数——父值、字段的参数、上下文和GraphQLResolveInfo
目的。如果我们要创建一个GraphQLObjectType
like userType
在上面的示例中,这是我们可以为对象中的每个字段提供的可选函数。这是same您在构造解析器映射时定义的函数以供使用graphql-yoga
. 这是字段解析器的唯一实现。
那么这是怎么回事buildSchema
??
文档中的示例利用了 GraphQL默认字段解析器 https://github.com/graphql/graphql-js/blob/86f6e821c803d7f1e22a99d7d22aab63c6c9f957/src/execution/execute.js#L1238:
export const defaultFieldResolver: GraphQLFieldResolver<any, *> = function(
source,
args,
contextValue,
info,
) {
if (typeof source === 'object' || typeof source === 'function') {
const property = source[info.fieldName];
if (typeof property === 'function') {
return source[info.fieldName](args, contextValue, info);
}
return property;
}
};
如您所见,默认解析逻辑会查找与源(父)值上的字段同名的属性。在我们上面的例子中,user
解析器返回{id: 1, email: '[email protected] /cdn-cgi/l/email-protection'}
-- 这是该字段的值resolves到。该字段的类型为User
。我们没有为我们定义解析器id
字段,因此默认解析器会执行其操作。这id
字段解析为1
因为这是名为的属性的值id
在解析器接收的父对象上。
然而,父值可以also是一个函数而不是一个对象。如果它是一个函数,则首先调用它,然后使用返回值。该函数是用什么来调用的?好吧,它不能向它传递父值(因为无限递归),但它can向其传递剩余的三个参数(args、context 和 info)。这就是它的作用。
现在来看看魔术 ????????
在我们的示例中,我可以省略解析器user
字段并将函数传递给根值。
const root = {
user: () => ({id: 1, email: '[email protected] /cdn-cgi/l/email-protection'})
}
根对象只是一个可选对象,它作为父值传递给根级别的解析器(就像您的Query
or Mutation
类型)。否则,这些解析器将没有父值。
Query
是一种操作根类型——它充当架构其余部分的“入口点”。上的任何字段Query
type 将作为父值传递给根对象。如果我省略了解析器user
字段,默认解析器将 1) 检查父对象是否有同名的属性,2) 查找属性并确定它是一个函数,3) 调用该函数,4) 将字段解析为函数的返回值。
TADA!
但是,由于该函数是由默认解析器调用的,并且本身不用作解析器,因此它只会接收上述三个参数,而不是 4 个。
这是解决无法解决问题的巧妙方法actually为模式提供自定义解析器,但它非常有限。它仅适用于根类型,因此我们不能类似地为User
字段或其他类型。我们不能在模式中使用接口或联合,因为我们无法提供resolveType
功能。等等...
希望这能提供一些清晰度。希望我们能够在不久的将来更新文档,以避免所有这些混乱。