graphql中resolver函数的不同实现的解释
Explanation for different implementations of resolver function in graphql
我一直在阅读 graphQL 文档,发现他们以两种方式解释了 graphql 服务器的实现:一种使用 graphql-yoga,这是一个功能齐全的 graphql 服务器,另一种使用 graphql, express-graphql 和 express.在这两种情况下,我们都会在创建服务器实例时传递模式和解析器函数。
但解析器函数的实现方式不同。在使用 graphql-yoga 时,解析器函数提供了 4 个参数,其中包含有关父对象、接收到的参数、上下文、信息的信息。而在另一种情况下(使用 graphql),解析器函数只获取参数对象。
为什么会这样?如果我想要信息、上下文对象,我该如何获取?
使用 graphql-yoga 示例:https://graphql.org/learn/execution/
使用 graphql 示例:https://graphql.github.io/graphql-js/mutations-and-input-types/
// 使用 graphql 的代码示例
var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');
var schema = buildSchema(`
type Query {
rollDice(numDice: Int!, numSides: Int): [Int]
}
type Mutation {
addDice(numDice: Int): String
}
`);
var root = {
rollDice({numDice, numSides}) {
return [1, 2];
},
addDice({numDice}) {
console.log("Adding something");
return "Added";
}
};
var app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
}));
app.listen(4000);
console.log('Running a GraphQL API server at localhost:4000/graphql');
// 使用 graphql-yoga 的代码示例
let graphqlServer = require("graphql-yoga");
const typeDefs = `
type Query {
rollDice(numDice: Int!, numSides: Int): [Int]
}
type Mutation {
addDice(numDice: Int): String
}
`;
const resolvers = {
Query: {
rollDice(parent, args, context, info) {
console.log(args.numDice);
console.log(args.numSides);
return [1, 2];
}
},
Mutation: {
addDice(parent, args, context, info) {
console.log(args.numDice);
return "Added";
}
}
};
const server = new graphqlServer.GraphQLServer({
typeDefs,
resolvers
});
server.start(() => {
console.log("server started on localhost:4000");
});
这两个代码片段的区别:
在一种情况下,解析器函数存在于适当的类型(即查询、突变)中。在另一种情况下,它们存在于一个根对象中。这意味着在第一种情况下我可以在 Query 和 Mutation 中使用相同名称的方法,而在第二种情况下这是不可能的,因为它们是单个对象的键并且键应该是唯一的。
为什么会这样?我基本上错过了什么吗?一个包与另一个包的实施细节有何不同?
真实对话:GraphQL.js 文档不是那么好。在我看来,他们一开始就不应该使用带有 buildSchema
的例子,因为它会导致这种混乱,这是可以理解的。
GraphQL.js(即 graphql
包)是 GraphQL 的 JavaScript 实现。通过构建 GraphQLSchema
class:
的实例,以编程方式在 GraphQL.js 中构建模式
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: 'john.doe@example.com' }),
},
},
});
const schema = new GraphQLSchema({
query: queryType,
})
如果我们用模式定义语言 (SDL) 打印此模式,它看起来像这样:
type Query {
user: User
}
type User {
id: ID
email: String
}
使用 SDL 比编写所有代码要容易得多。 但是,GraphQL.js 不提供从 SDL 构建功能齐全的模式的方法。它确实提供了 buildSchema
函数,但此实用程序构建了一个模式 ,没有任何解析器 (以及许多其他功能,如 union/interface 类型解析)。
graphql-tools
包提供了一个 makeExecutableSchema
功能,可让您从 SDL 和解析器映射对象构建模式。这就是 apollo-server
和 graphql-yoga
在后台使用的内容。 makeExecutableSchema
使用 buildSchema
从 SDL 构造一个模式,然后改变生成的对象,在 after the fact.
中添加解析器
在 GraphQL.js 中,字段的 resolve
函数(或解析器)有四个参数——父值、字段的参数、上下文和一个 GraphQLResolveInfo
对象。如果我们在上面的例子中创建一个 GraphQLObjectType
就像 userType
一样,这是我们可以为对象中的每个字段提供的可选函数。这是您在构造解析器映射以与 graphql-yoga
一起使用时定义的 相同 函数。 这是字段解析器的唯一实现。
那么 buildSchema
是怎么回事??
文档中的示例利用了 GraphQL 的 default field resolver:
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
解析器 returns {id: 1, email: 'john.doe@example.com'}
—— 这是字段 解析 的值。该字段的类型为 User
。我们没有为 id
字段定义解析器,因此默认解析器会执行它的操作。 id
字段解析为 1
,因为这是解析器接收的父对象上名为 id
的 属性 的值。
但是,父值可以也是函数而不是对象。如果它是一个函数,它首先被调用,然后使用 return 值。该函数是用什么调用的?好吧,它不能向它传递父值(因为无限递归),但它可以 向它传递其余三个参数(args、上下文和信息)。这就是它的作用。
魔术表演开始
在我们的示例中,我可以省略 user
字段的解析器,而是将函数传递给根值。
const root = {
user: () => ({id: 1, email: 'john.doe@example.com'})
}
根对象只是一个可选对象,作为父值传递给根级别的解析器(如您的 Query
或 Mutation
类型)。否则,这些解析器将没有父值。
Query
是一种可操作的根类型——它作为您模式其余部分的 "entry point"。 Query
类型的任何字段都将作为父值传递给根对象。如果我省略 user
字段的解析器,则默认解析器将 1) 检查父对象中是否有同名的 属性,2) 找到 属性 并确定它是函数,3) 调用函数,4) 将字段解析为函数的 return 值。
田田!
但是,由于该函数是由默认解析器调用的,并且本身不用作解析器,因此它只会接收上述三个参数,而不是 4 个。
这是解决无法实际上 为架构提供自定义解析器的巧妙方法,但它非常有限。它仅适用于根类型,因此我们不能类似地为 User
字段或其他类型提供伪造的解析器。我们不能在我们的模式中使用接口或联合,因为我们不能提供 resolveType
函数。等等...
希望这能提供一些清晰度。希望我们能在不久的将来更新文档,首先避免所有这些混乱。
我一直在阅读 graphQL 文档,发现他们以两种方式解释了 graphql 服务器的实现:一种使用 graphql-yoga,这是一个功能齐全的 graphql 服务器,另一种使用 graphql, express-graphql 和 express.在这两种情况下,我们都会在创建服务器实例时传递模式和解析器函数。
但解析器函数的实现方式不同。在使用 graphql-yoga 时,解析器函数提供了 4 个参数,其中包含有关父对象、接收到的参数、上下文、信息的信息。而在另一种情况下(使用 graphql),解析器函数只获取参数对象。
为什么会这样?如果我想要信息、上下文对象,我该如何获取?
使用 graphql-yoga 示例:https://graphql.org/learn/execution/
使用 graphql 示例:https://graphql.github.io/graphql-js/mutations-and-input-types/
// 使用 graphql 的代码示例
var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');
var schema = buildSchema(`
type Query {
rollDice(numDice: Int!, numSides: Int): [Int]
}
type Mutation {
addDice(numDice: Int): String
}
`);
var root = {
rollDice({numDice, numSides}) {
return [1, 2];
},
addDice({numDice}) {
console.log("Adding something");
return "Added";
}
};
var app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
}));
app.listen(4000);
console.log('Running a GraphQL API server at localhost:4000/graphql');
// 使用 graphql-yoga 的代码示例
let graphqlServer = require("graphql-yoga");
const typeDefs = `
type Query {
rollDice(numDice: Int!, numSides: Int): [Int]
}
type Mutation {
addDice(numDice: Int): String
}
`;
const resolvers = {
Query: {
rollDice(parent, args, context, info) {
console.log(args.numDice);
console.log(args.numSides);
return [1, 2];
}
},
Mutation: {
addDice(parent, args, context, info) {
console.log(args.numDice);
return "Added";
}
}
};
const server = new graphqlServer.GraphQLServer({
typeDefs,
resolvers
});
server.start(() => {
console.log("server started on localhost:4000");
});
这两个代码片段的区别:
在一种情况下,解析器函数存在于适当的类型(即查询、突变)中。在另一种情况下,它们存在于一个根对象中。这意味着在第一种情况下我可以在 Query 和 Mutation 中使用相同名称的方法,而在第二种情况下这是不可能的,因为它们是单个对象的键并且键应该是唯一的。
为什么会这样?我基本上错过了什么吗?一个包与另一个包的实施细节有何不同?
真实对话:GraphQL.js 文档不是那么好。在我看来,他们一开始就不应该使用带有 buildSchema
的例子,因为它会导致这种混乱,这是可以理解的。
GraphQL.js(即 graphql
包)是 GraphQL 的 JavaScript 实现。通过构建 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: 'john.doe@example.com' }),
},
},
});
const schema = new GraphQLSchema({
query: queryType,
})
如果我们用模式定义语言 (SDL) 打印此模式,它看起来像这样:
type Query {
user: User
}
type User {
id: ID
email: String
}
使用 SDL 比编写所有代码要容易得多。 但是,GraphQL.js 不提供从 SDL 构建功能齐全的模式的方法。它确实提供了 buildSchema
函数,但此实用程序构建了一个模式 ,没有任何解析器 (以及许多其他功能,如 union/interface 类型解析)。
graphql-tools
包提供了一个 makeExecutableSchema
功能,可让您从 SDL 和解析器映射对象构建模式。这就是 apollo-server
和 graphql-yoga
在后台使用的内容。 makeExecutableSchema
使用 buildSchema
从 SDL 构造一个模式,然后改变生成的对象,在 after the fact.
在 GraphQL.js 中,字段的 resolve
函数(或解析器)有四个参数——父值、字段的参数、上下文和一个 GraphQLResolveInfo
对象。如果我们在上面的例子中创建一个 GraphQLObjectType
就像 userType
一样,这是我们可以为对象中的每个字段提供的可选函数。这是您在构造解析器映射以与 graphql-yoga
一起使用时定义的 相同 函数。 这是字段解析器的唯一实现。
那么 buildSchema
是怎么回事??
文档中的示例利用了 GraphQL 的 default field resolver:
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
解析器 returns {id: 1, email: 'john.doe@example.com'}
—— 这是字段 解析 的值。该字段的类型为 User
。我们没有为 id
字段定义解析器,因此默认解析器会执行它的操作。 id
字段解析为 1
,因为这是解析器接收的父对象上名为 id
的 属性 的值。
但是,父值可以也是函数而不是对象。如果它是一个函数,它首先被调用,然后使用 return 值。该函数是用什么调用的?好吧,它不能向它传递父值(因为无限递归),但它可以 向它传递其余三个参数(args、上下文和信息)。这就是它的作用。
魔术表演开始
在我们的示例中,我可以省略 user
字段的解析器,而是将函数传递给根值。
const root = {
user: () => ({id: 1, email: 'john.doe@example.com'})
}
根对象只是一个可选对象,作为父值传递给根级别的解析器(如您的 Query
或 Mutation
类型)。否则,这些解析器将没有父值。
Query
是一种可操作的根类型——它作为您模式其余部分的 "entry point"。 Query
类型的任何字段都将作为父值传递给根对象。如果我省略 user
字段的解析器,则默认解析器将 1) 检查父对象中是否有同名的 属性,2) 找到 属性 并确定它是函数,3) 调用函数,4) 将字段解析为函数的 return 值。
田田!
但是,由于该函数是由默认解析器调用的,并且本身不用作解析器,因此它只会接收上述三个参数,而不是 4 个。
这是解决无法实际上 为架构提供自定义解析器的巧妙方法,但它非常有限。它仅适用于根类型,因此我们不能类似地为 User
字段或其他类型提供伪造的解析器。我们不能在我们的模式中使用接口或联合,因为我们不能提供 resolveType
函数。等等...
希望这能提供一些清晰度。希望我们能在不久的将来更新文档,首先避免所有这些混乱。