深入理解GraphQL类型系统:构建高效API的关键

开场白

GraphQL 是一种促进前端与数据源通信的现代解决方案。所有关于 GraphQL 实现的细节和功能都在 GraphQL Schema 中定义。为了编写一个功能完整的 GraphQL 模式,您必须了解 GraphQL 类型系统。

在这篇文章中,您将了解有关 GraphQL 类型的内容:五种内置标量类型、枚举类型、列表和非空类型、对象类型以及与它们一起使用的抽象接口和联合类型。您将会通过每种类型的示例进行复习,并学习如何使用它们来构建一个完整的 GraphQL 模式。

先决条件

为了从本教程中获益最大化,您应该具备以下条件:

  • 对 GraphQL 的基本概念有一定了解,可参考《GraphQL 入门指南》。
  • 一个 GraphQL 环境,例如《如何在 Node.js 中搭建 GraphQL API 服务器》中所示。

标量类型

一个 GraphQL Schema 中的所有数据最终都会解析为不同的标量类型,这些类型代表原始值。GraphQL 的响应可以被表示为一棵树,而标量类型则是树末端的叶子节点。在一个嵌套的响应中可以有多个层级,但是最后一层总是会解析为标量(或者枚举)类型。GraphQL 内置了五种标量类型:Int、Float、String、Boolean 和 ID。

整型 (Int)

Int 是一个带符号的 32 位非小数数值。它是一个带符号(正数或负数)的整数,不包括小数部分。带符号 32 位整数的最大值是 2,147,483,647。这是用于数字数据的两个内置标量之一。

浮点型 (Float)

Float 是一种有符号的双精度小数值。它是一个带有小数点的有符号(正或负)数字,例如 1.2。这是另一种用于数值数据的内置标量。

字符串 (String)

String 是一个 UTF-8 字符序列。字符串类型用于任何文本数据。这也可以包括非常大的数字等数据。大多数自定义标量将是字符串数据类型。

布尔型 (Boolean)

Boolean 是一个真或假的值。

ID 型 (ID)

ID 是一个唯一标识符。无论 ID 是否为数字,这个值总是被序列化为字符串。ID 类型可能经常用通用唯一标识符(UUID)表示。

自定义标量

除了这些内置的标量之外,scalar 关键字还可以用于定义自定义标量。您可以使用自定义标量来创建具有附加服务器级验证的类型,例如日期、时间或 URL。以下是定义新日期类型的示例:

scalar Date

服务器将知道如何使用 GraphQLScalarType 来处理与这种新类型的交互。

枚举类型

枚举类型,也被称为“枚举器”类型,描述了一组可能的值。

在《如何使用 GraphQL 管理数据》系列的其他教程中使用了 Fantasy Game API 主题,你可以为游戏角色的职业和物种创建一个包含系统允许的所有值的枚举。枚举类型使用 enum 关键字进行定义,如下所示:

"The job class of the character."
enum Job {
  FIGHTER
  WIZARD
}

"The species or ancestry of the character."
enum Species {
  HUMAN
  ELF
  DWARF
}

用这种方式可以保证一个角色的职业是“战士”或者“法师”,而不会出现“紫色”或其他随机字符串的情况,如果使用字符串类型而不是自定义枚举,这种情况可能发生。按照惯例,枚举类型的名称应该全部大写。

枚举也可以用作参数中可接受的值。例如,您可以创建一个 Hand 枚举来表示武器是单手(如短剑)还是双手(如重斧),并使用它来确定是否可以装备一把或两把武器。

enum Hand {
  SINGLE
  DOUBLE
}

"A valiant weapon wielded by a fighter."
type Weapon {
  name: String!
  attack: Int
  range: Int
  hand: Hand
}

type Query {
  weapons(hand: Hand = SINGLE): [Weapon]
}

Hand 的枚举类型已经声明为单手 (SINGLE) 和双手 (DOUBLE) 两个值,并且 weapons 字段的参数默认值为单手 (SINGLE),意味着如果没有传入参数,则会回退到单手 (SINGLE)。

非空类型

你可能注意到,在内置的标量类型列表中,空或未定义的 null 是常见的类型,许多编程语言都认为它是一种原始的数据类型。然而,在 GraphQL 的内置标量类型列表中,并没有包含 null。在 GraphQL 中,null 代表缺少具体数值的情况。

GraphQL 中的所有类型默认都是可为空的,因此 null 对于任何类型都是有效的响应。为了使值变为必需,它必须转换为带有尾部感叹号的 GraphQL 非空类型。非空类型被定义为类型修饰符,这些修饰符用于修改所引用的类型。例如,String 是一个可选(或可为空)的字符串,而 String! 是一个必需(或非空)的字符串。

列表类型

在 GraphQL 中,List 类型是另一种类型修饰符。任何用方括号([])包裹的类型都会变成 List 类型,它是一种集合,定义了列表中每个项目的类型。

例如,被定义为 [Int] 的类型将是 Int 类型的集合,而 [String] 将是 String 类型的集合。非空和列表可以结合使用,使类型既是必需的,又被定义为一个列表,例如 [String]!

对象类型

这是文章《对GraphQL类型系统的理解》的第2部分(共5部分)。

如果GraphQL标量类型描述了层次化GraphQL响应的“叶子”,那么对象类型则描述了中间的“分支”。实际上,GraphQL模式中的几乎所有内容都是对象类型的一种。

对象由一系列命名字段(键)和每个字段将解析为的值类型组成。对象使用type关键字定义。必须至少定义一个或多个字段,且字段不能以双下划线(__)开头,以避免与GraphQL内省系统冲突。

在GraphQL幻想游戏API示例中,您可以创建一个Fighter对象来代表游戏中的角色类型。

"一个具有直接战斗能力和力量的英雄。"
type Fighter {
  id: ID!
  name: String!
  level: Int
  active: Boolean!
}

在这个示例中,已声明了Fighter对象类型,它有四个命名字段:

  • id:解析为非空ID类型。
  • name:解析为非空String类型。
  • level:解析为Int类型。
  • active:解析为非空Boolean类型。

在声明之上,您还可以使用双引号添加注释,例如:"一个具有直接战斗能力和力量的英雄。"。这将在类型描述中显示出来。

在这个例子中,每个字段都解析为一个标量类型,但是对象字段也可以解析为其他对象类型。例如,您可以创建一个Weapon类型,GraphQL模式可以被设置为战士的weapon字段解析为一个Weapon对象。

"战士所使用的英勇武器。"
type Weapon {
  name: String!
  attack: Int
  range: Int
}

"一个具有直接战斗能力和力量的英雄。"
type Fighter {
  id: ID!
  name: String!
  level: Int
  active: Boolean!
  weapon: Weapon
}

对象也可以嵌套在其他对象的字段中。

根操作类型

这是文章《对GraphQL类型系统的理解》的第3部分(共5部分)。

在GraphQL模式中,有三个特殊对象作为入口点:Query(查询)、Mutation(变更)和Subscription(订阅)。它们被称为根操作类型,并遵循与其他任何对象类型相同的规则。

schema 关键字代表GraphQL模式的入口点。您的根查询(Query)、变更(Mutation)和订阅(Subscription)类型将存在于根模式对象上。

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

查询类型(Query)在任何GraphQL架构中都是必需的,表示一个读取请求,类似于REST API的GET请求。以下是一个返回Fighter类型列表的根查询对象示例。

type Query {
  fighters: [Fighter]
}

变更类型(Mutation)代表一个写入请求,类似于REST API中的POST、PUT或DELETE操作。在下面的示例中,Mutation具有一个addFighter字段,带有一个命名参数(input)。

type Mutation {
  addFighter(input: FighterInput): Fighter
}

最后,订阅类型(Subscription)对应于事件流,在Web应用中通常与WebSocket一起使用。在GraphQL Fantasy API中,它或许可以用于随机战斗遭遇,如下所示:

type Subscription {
  randomBattle(enemy: Enemy): BattleResult
}

请注意,在一些GraphQL实现中,模式入口点常常被封装起来。

字段参数

GraphQL对象的字段本质上是返回值的函数,并且它们可以像任何函数一样接受参数。字段参数由参数名称后跟类型来定义。参数可以是任何非对象类型。在这个例子中,Fighter对象可以通过id字段进行筛选(该字段解析为非空ID类型)。

type Query {
  fighter(id: ID!): Fighter
}

这个特定的例子用于从数据存储中获取单个项目,但参数也可以用于过滤、分页和其他更具体的查询。

接口类型

这是文章《对GraphQL类型系统的理解》的第4部分(共5部分)。

与对象类型相似,抽象的接口类型由一系列具有名称的字段和它们关联的值类型组成。接口的外观和规则与对象相同,但用于定义对象实现的一个子集。

到目前为止,在你的架构中,你有一个战士对象,但你可能还想创建一个法师、一个治疗师和其他一些对象,它们将共享许多相同的字段,但也有一些不同之处。在这种情况下,你可以使用接口来定义它们共有的字段,并创建实现该接口的对象。

在以下示例中,您可以使用 interface 关键字创建一个 BaseCharacter 接口,其中包含每种角色都具备的所有字段。

"一个正在执行任务的英雄。"
interface BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job
}

每个角色类型都会拥有 idname(姓名)、level(等级)、species(物种)和 job(职业)等字段。

现在,想象一下你有一个战士类型和一个法师类型,它们具有相同的字段,但是战士使用武器,而法师使用法术。你可以使用 implements 关键字将它们定义为 BaseCharacter 的实现类,这意味着它们必须具有被创建的接口的所有字段。

"一个拥有直接战斗能力和力量的英雄。"
type Fighter implements BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job!
  weapon: Weapon
}

"一个拥有各种魔法能力的英雄。"
type Wizard implements BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job!
  spells: [Spell]
}

战士和法师都是 BaseCharacter 接口的有效实现,因为它们具备所需的字段子集。

联合类型

还有另一种可以与对象一起使用的抽象类型,那就是联合类型(Union Type)。使用 union 关键字,您可以定义一个包含多个对象的类型,这些对象都可以作为有效的响应。

在之前的部分创建的接口中,你可以创建一个角色联合体,将角色定义为巫师或战士。

union Character = Wizard | Fighter

等号(=)表示定义,而竖线(|)表示“或”语句。请注意,联合类型必须由对象或接口组成,标量类型在联合类型中无效。

现在如果您查询字符列表,它可以使用字符联合,返回所有巫师和战士类型。

结论

这是文章《对 GraphQL 类型系统的理解》的第 5 部分(共 5 部分)。

在本教程中,你了解了 GraphQL 类型系统中定义的多种类型。最基础的类型是标量类型,它们是模式树上的叶子节点(对应的值),包括整型(Int)、浮点型(Float)、字符串(String)、布尔型(Boolean)、ID 类型,以及 GraphQL 实现可以创建的任何自定义标量。枚举类型是有效常量值的列表,当你需要比简单声明字符串更精确地控制响应时可以使用,并且也是模式树上的叶子节点。列表类型非空类型被称为类型修饰符或封装类型,它们可以定义其他类型作为集合或必须存在的类型。对象类型是模式树的分支,几乎 GraphQL 模式中的所有内容都是对象类型,包括查询(Query)、变更(Mutation)和订阅(Subscription)入口点。接口类型联合类型是抽象类型,可以帮助定义对象。

为了进一步学习,你可以通过阅读《如何在 Node.js 设置 GraphQL API 服务器》的教程来练习创建和修改 GraphQL 模式,以获得一个可工作的 GraphQL 服务器环境。

bannerAds