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

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

相关文章

目录

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

接口

考虑如果在一个查询的返回结果中有两种对象,这两种对象有一部分字段是相同的,也有各自特有的字段。按照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框架来简化开发,如ApolloRelay

本文参考资源如下

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容