【原创】GraphQL学习:接口、联合类型、输入类型

GraphQL中数据类型分为标量类型和其他高级数据类型,标量类型即基础数据类型,包含Int、Float、String、Boolean、ID,其他高级数据类型包括Object(对象)、Interface(接口)、Union(联合类型)、Enum(枚举)、Input Object(输入类型)、List(列表)、Non-Null(非空类型),本文通过示例讲解接口、联合类型、输入类型的使用。

相关文章

  • GraphQL学习:入门
  • GraphQL学习:分页
  • GraphQL学习:接口、联合类型、输入类型

目录

  • 接口
  • 联合类型
  • 输入类型

接口

考虑如果在一个查询的返回结果中有两种对象,这两种对象有一部分字段是相同的,也有各自特有的字段。按照GraphQL中正常对象的写法,必须得写出查询所有需要返回的字段,这会导致返回的结果中有一些空值字段。在此情况下可引入接口类型,在接口中声明公用的字段,其他的对象可实现接口,再添加特有的字段。

在GraphQL中,接口的字段不能自动继承,需要在每个实现的接口中都声明接口中的字段,并且一个对象可以实现多个接口。

定义一个接口如下:

// 定义公用字段,因为每个接口都要声明公用字段,所以单独提取出来
const messageFields = {
  id: { type: GraphQLInt },
  type: { type: GraphQLString },
  content: { type: GraphQLString },
  createAt: { type: GraphQLString }
}
// 定义接口对象
const Message = new GraphQLInterfaceType({
  name: 'Message',
  fields: messageFields,
  // 用于判断实际的数据是哪种实现类型
  resolveType: (value) => {
    if (value.type === 'user') return UserMessage
    if (value.type === 'group') return GroupMessage
  }
})

在根据实际的数据判断具体是哪个实现对象时,可以根据接口中声明的resolveType来判断,如果接口中不存在该方法,则根据实现该接口的对象中的isTypeOf方法来判断,如:

const UserMessage = new GraphQLObjectType({
  name: 'UserMessage',
  interfaces: [Message], // 可实现多个接口
  fields: {
    ...messageFields,
    userName: { type: GraphQLString }
  },
 // 判断数据是否为该对象类型,如果实现的接口中实现了resolveType方法,此处可以不写
  isTypeOf: value => value.type === 'user' 
})

const GroupMessage = new GraphQLObjectType({
  name: 'GroupMessage',
  interfaces: [Message], // 可实现多个接口
  fields: {
    ...messageFields,
    groupName: { type: GraphQLString }
  }
})

构造查询方法和数据:

const initialDate = moment('2019-01-01')
const messages = Mock.mock({
  'list|5': [{
    'id|+1': 1000,
    'type': /user|group/,
    'content': /msg-[a-z]{5,10}/,
    'createAt': () => initialDate
      .add(Mock.Random.integer(1000, 10000), 'seconds')
      .format('YYYY-MM-DD HH:mm:ss')
  }]
}).list

const queryObjectType = new GraphQLObjectType({
  name: 'RootQuery',
  fields: {
    messages: {
      type: new GraphQLList(Message), // 此处使用接口数据类型
      resolve (parent) {
        return messages.map(one => {
          if (one.type === 'group') {
            return {
              ...one,
              // 构造群组消息特有字段
              groupName: 'group_' + Mock.Random.string(3)
            }
          }
          if (one.type === 'user') {
            return {
              ...one,
              // 构造用户消息特有字段
              userName: 'user_' + Mock.Random.string(5, 10)
            }
          }
          return one
        })
      }
    }
  }
})

如果直接运行以上的代码,会出现如下错误:

接口类型示例查询结果

由于GraphQL中不能根据对象声明所实现的interfaces来找到一个接口类型有哪些实现对象,因此还需要显示的指定有哪些可查找的对象类型,然后根据resolveType或者遍历指定可查找的对象的isTypeOf来获取数据对应的实现对象。指定对象类型如下:

const schema = new GraphQLSchema({
  types: [Message, UserMessage, GroupMessage],
  query: queryObjectType
})

以上已经实现了服务端的接口类型的示例,接下来需要构造GraphQL查询语句来查询接口类型的数据。

如果只需要返回接口公用的数据,查询语句如下:

{
  messages {
    id
    type
    content
    createAt
    # 接口类型中默认会添加上__typename字段,表示最终实现接口的对象类型
    __typename
  }
}

查询结果如下:

接口类型示例查询结果

当需要返回接口实现对象的特殊字段时,需要使用... on的语法,表示如果返回对象为该类型时,取该类型中的特定字段。查询语句示例如下:

{
  messages {
    id
    type
    content
    createAt
    ... on UserMessage {
       userName
    }
    ... on GroupMessage {
      groupName
    }
  }
}

查询结果如下:

接口类型示例查询结果

联合类型

有时候在一个查询的结果里返回的类型可能是两种不同的对象,在指定返回字段时应根据实际的类型来指定返回哪些字段,这时可以使用联合类型。

首先定义两种对象类型,它们可以有相同的字段,也可以没有。如果相同字段过多其实就是接口类型的使用场景了。

// 这里同样以消息对象为例
// 虽然它们有公共字段,但可以不使用接口类型处理
const messageFields = {
  id: { type: GraphQLInt },
  type: { type: GraphQLString },
  content: { type: GraphQLString },
  createAt: { type: GraphQLString }
}
const UserMessage = new GraphQLObjectType({
  name: 'UserMessage',
  fields: {
    ...messageFields,
    userName: { type: GraphQLString }
  }
})
const GroupMessage = new GraphQLObjectType({
  name: 'GroupMessage',
  fields: {
    ...messageFields,
    groupName: { type: GraphQLString }
  }
})

以上文定义的UserMessageGroupMessage对象,定义一个联合类型的对象如下:

const Message = new GraphQLUnionType({
  name: 'Message',
  types: [UserMessage, GroupMessage],
  // 与接口类型一样,用于判断实际的数据是哪种类型
  resolveType: (value) => {
    if (value.type === 'user') return UserMessage
    if (value.type === 'group') return GroupMessage
  }
})

这里的resolveType使用和接口类型完全一致,如果联合类型中不存在该方法,则根据联合类型中指定的对象的isTypeOf方法来判断。

构造查询方法和数据与上文接口类型中的方法完全一致,最后同样需要在schema中指定联合查询中使用了哪些对象,如下:

const schema = new GraphQLSchema({
  types: [Message, UserMessage, GroupMessage],
  query: queryObjectType
})

查询联合类型的对象时,除了没有公共字段外,其他与接口类型的查询一致,也是使用... on语法来查询,查询语句如下:

{
  messages {
    ...on UserMessage {
      id
      userName
      content
    }
    ...on GroupMessage {
      id
      groupName
      content
    }
  }
}

查询结果如下:

接口类型示例查询结果

接口类型和联合类型在使用体验上基本相同,通常是看公共字段的多少来决定是否使用接口提取公共字段。

输入类型

在之前的查询参数中,基本上都是使用的标量类型,如果要表示查询参数是对象类型,需要使用输入类型。

定义一个输入类型如下:

const UserInput = new GraphQLInputObjectType({
  name: 'UserInput',
  fields: {
    id: { type: GraphQLInt },
    name: { type: GraphQLString },
    age: { type: GraphQLInt },
    labels: { type: GraphQLString }
  }
})

输入类型和定义一个对象类型是一致的,不过输入类型可设置默认值,如下:

const PaginationInput = new GraphQLInputObjectType({
  name: 'PaginationInput',
  fields: {
    page: { type: GraphQLInt, defaultValue: 1 },
    pageSize: { type: GraphQLInt, defaultValue: 3 }
  }
})

使用上面定义的两个输入类型作为查询参数的类型,示例如下:

const users = Mock.mock({
  'list|100': [{
    'id|+1': 1000,
    'name': /user-[a-zA-Z]{4}/,
    'age|1-100': 100,
    'labels': () => ['sportsman', 'programmer', 'teacher', 'musician', 'chef'].filter(() => Math.random() < 0.3)
  }]
}).list
const User = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: { type: GraphQLInt },
    name: { type: GraphQLString },
    age: { type: GraphQLInt },
    labels: { type: new GraphQLList(GraphQLString) }
  }
})
const UserPagination = new GraphQLObjectType({
  name: 'UserPagination',
  fields: {
    data: { type: new GraphQLList(User) },
    totalCount: { type: GraphQLInt },
    totalPageCount: { type: GraphQLInt }
  }
})
// 查询的GraphQL对象类型
const queryObjectType = new GraphQLObjectType({
  name: 'RootQuery',
  fields: {
    users: { // 条件查询
      type: UserPagination,
      args: {
        user: { type: UserInput }, // 输入类型
        pagination: { type: PaginationInput } // 输入类型
      },
      resolve (parent, { user: userInput, pagination }) {
        const rules = Object.keys(userInput).map(one => {
          if (one === 'labels') return user => user[one].includes(userInput[one])
          if (one === 'name') return user => user[one].indexOf(userInput[one]) > -1
          return user => user[one] === userInput[one]
        })
        const { page, pageSize } = pagination
        const totalData = users.filter(one => rules.every(rule => rule(one)))
        const data = totalData.slice((page - 1) * pageSize, page * pageSize)
        const { length: total } = totalData
        return {
          data,
          totalCount: total,
          totalPageCount: Math.ceil(total / pageSize)
        }
      }
    }
  }
})

构造查询语句,查询标签中包含"teacher",查询结果为第二页的数据,如下:

{
  users(user: {labels: "teacher"}, pagination: {page: 2}) {
    data {
      id
      name
      age
      labels
    }
    totalCount
    totalPageCount
  }
}

查询结果如下:

输入类型查询结果

总结

本文主要介绍了接口、联合类型、输入类型这三种高级数据类型的使用,使用起来还是比较简单的,不过由于官方文档中没有实际的示例演示,于是通过编写示例的方式加强对GraphQL中数据类型的使用和理解。

到目前为止,学习的GraphQL都是比较基础的内容,不过也算初步入门。其中最重要的收获应该是对GraphQL有了比较清晰的认识,为以后的技术选型打下基础。在实际项目中使用GraphQL通常也是使用社区里的GraphQL框架来简化开发,如Apollo、Relay。

本文参考资源如下

  • https://graphql.cn/learn/schema/
  • https://graphql.cn/graphql-js/type/
  • http://taobaofed.org/blog/2016/03/10/graphql-in-depth/

你可能感兴趣的:(【原创】GraphQL学习:接口、联合类型、输入类型)