graphql中解析器函数的不同实现说明

Posted

技术标签:

【中文标题】graphql中解析器函数的不同实现说明【英文标题】:Explanation for different implementations of resolver function in graphql 【发布时间】:2019-09-15 04:33:21 【问题描述】:

我一直在阅读 graphQL 文档,发现他们以两种方式解释了 graphql 服务器的实现:一种使用 graphql-yoga,它是一个功能齐全的 graphql 服务器,另一种是使用 graphql,express- graphql 和快递。在这两种情况下,我们都会在创建服务器实例时传递架构和解析器函数。

但是解析器功能的实现不同。在使用 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");
);

这2个代码sn-ps的区别:

在一种情况下,解析器函数存在于适当的类型(即查询、突变)中。在另一种情况下,它们存在于一个根对象中。这意味着在第一种情况下我可以在 Query 和 Mutation 中使用同名的方法,而在第二种情况下这是不可能的,因为它们是单个对象的键并且键应该是唯一的。

为什么会这样?我基本上错过了什么吗?不同包的实现细节有何不同?

【问题讨论】:

【参考方案1】:

真实对话:GraphQL.js 文档不是那么好。在我看来,他们一开始就不应该使用带有buildSchema 的示例,因为它会导致这种混乱,这是可以理解的。

GraphQL.js(即graphql 包)是 GraphQL 的 javascript 实现。在 GraphQL.js 中构建模式是通过构建 GraphQLSchema 类的实例以编程方式完成的:

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 函数,但是这个实用程序构建了一个架构 没有任何解析器(以及许多其他功能,例如联合/接口类型解析)。

graphql-tools 包提供了一个makeExecutableSchema 函数,可让您从 SDL 和解析器映射对象构建架构。这是apollo-servergraphql-yoga 在后台使用的。 makeExecutableSchema 使用 buildSchema 从 SDL 构造一个模式,然后改变生成的对象,在 after the fact 中添加解析器。

在 GraphQL.js 中,字段的 resolve 函数(或解析器)采用四个参数——父值、字段的参数、上下文和 GraphQLResolveInfo 对象。如果我们在上面的例子中像userType 一样创建GraphQLObjectType,这是我们可以为对象中的每个字段提供的可选函数。这是您在构造解析器映射以与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 解析器返回id: 1, email: 'john.doe@example.com'——这是字段解析到的值。该字段的类型为User。我们没有为 id 字段定义解析器,所以默认解析器会做它的事情。 id 字段解析为 1,因为这是解析器接收到的父对象上名为 id 的属性的值。

然而,父值也可以是一个函数而不是一个对象。如果它是一个函数,则首先调用它,然后使用返回值。该函数是用什么调用的?好吧,它不能向它传递父值(因为无限递归),但它可以向它传递剩余的三个参数(args、context 和 info)。这就是它的作用。

现在开始魔术了 ??

在我们的示例中,我可以省略 user 字段的解析器,而是将函数传递给根值。

const root = 
  user: () => (id: 1, email: 'john.doe@example.com')

根对象只是一个可选对象,它作为父值传递给根级别的解析器(如您的QueryMutation 类型)。否则,这些解析器将没有父值。

Query 是一种可操作的根类型——它充当您架构其余部分的“入口点”。 Query 类型上的任何字段都将作为父值传递给根对象。如果我省略了 user 字段的解析器,默认解析器将 1) 检查父对象的同名属性,2) 找到一个属性并确定它是一个函数,3) 调用该函数,4)将字段解析为函数的返回值。

TADA

但是,由于该函数是由默认解析器调用的,并且本身并不用作解析器,所以它只会接收上述三个参数,而不是4个。

这是解决无法实际上为模式提供自定义解析器的一种巧妙方法,但它非常有限。它仅适用于根类型,因此我们不能类似地为 User 字段或其他类型提供假解析器。我们不能在我们的模式中使用接口或联合,因为我们不能提供resolveType 函数。等等……

希望这能提供一些清晰的信息。希望我们可以在不久的将来更新文档,以避免所有这些混乱。

【讨论】:

谢谢@daniel-rearden 这让我更清楚了。希望他们尽快更新文档 xD 我一整天都在寻找这个确切的信息。这是一个很好的总结,解决了关于默认解析器、buildSchema 与 makeExecutableSchema 等所有这些混淆点。非常感谢 Daniel。我完全同意大部分内容应该在文档中。

以上是关于graphql中解析器函数的不同实现说明的主要内容,如果未能解决你的问题,请参考以下文章

如何从需要 return 语句的 GraphQL 解析器中调用异步 node.js 函数?

在 graphql-yoga 中返回枚举的解析器函数是啥样的?

Nestjs中多个graphql解析器实现的问题

输入 graphql 解析器函数

解析器必须是对象或函数(Graphql 关系)

用于可迭代对象的 Java GraphQL 解析器:休眠异常