编写GraphQL模式的十个提示

104 阅读12分钟

编写第一个GraphQL模式的十个提示

有一天,我的一个同事给我发来消息,问我对那些编写第一个GraphQL模式的人是否有什么提示。我回想了一下我在过去三年里所学到的关于GraphQL模式的一切。我犯了哪些错误?我希望我提前知道什么?我看到有哪些事情绊住了其他开发者?

我给他回复了一个建议,然后是另一个,再然后是另一个。在我知道之前,我基本上已经写了一整篇博客文章的内容。因此,不再多说,这里是我给那些编写第一个GraphQL模式的人的十大建议。剧透一下:在第七条提示中,我对日期格式变得相当激动。

1.1.确保你使用GraphQL的原因是正确的

像任何新技术一样,学习和使用GraphQL需要努力。因此,重要的是,要确保你能从这一努力中获得足够的好处。

在我看来,Facebook设计GraphQL的主要目的是:当用户在客户端执行某些操作时,客户端可以通过互联网获取所需的数据,不多不少,只需一次调用。此外,Facebook希望让客户尽可能容易地保留这一特性,即使他们的数据需求随着时间的推移而变化。

我提到这一点是因为GraphQL也可以用在其他有趣的方面,但不一定与最初的目标有关。例如,它可以被用作。

然而,我还不相信GraphQL一定是实现这些结果的最佳方式。例如,如果你想为微服务之间的通信提供一个REST的替代方案,你可能最好看看gRPC。或者,如果你使用GraphQL框架将数据源直接暴露给客户端,但需要从多个来源获得数据以响应单一的用户交互,客户端最终会在互联网上进行多次调用,这就违背了首先使用GraphQL的目的。

因此,在你开始之前,要清楚你为什么要使用GraphQL。如果GraphQL最初的主要好处--高效、灵活地获取用户在某一时间点所需的数据--实际上并不是你最感兴趣的事情,那么也许GraphQL并不适合于你。

2.理解术语

好吧,让我们假设你确定GraphQL是你所需要的。现在你实际上需要了解它。

GraphQL模式和查询构成了一种小型的、专门的编程语言,其目标与大多数开发人员习惯于日常使用的通用语言不同。一些结构体可能有听起来很熟悉的名字,但这并不意味着它们的意思正是你所认为的那样。还有一些结构体代表了你以前从未见过的概念。

为了完成工作,避免理解这些概念可能是很诱人的。这种实用主义是令人钦佩的,但只在某一点上有效。最终,就像任何编程语言一样,你需要发展对基础知识的理解,这样,当你进入更高级的东西时,你就有了基础。

例如,在编写模式时,理解什么是类型和字段,以及什么是参数是很重要的。当编写查询时,了解什么是操作变量是很重要的。要了解任何类型的字段可以接受参数,而不仅仅是Query 类型的字段。还要理解QueryMutation 就像其他类型一样,唯一的例外是GraphQL服务器必须能够在传入的请求中把querymutation 操作名称映射到它们身上。

为了理解这一切(以及更多),我建议任何使用GraphQL的人在某个阶段阅读文档。你不必一气呵成,但当你设计你的第一个模式和编写你的第一个查询时,我建议阅读(必要时重读)文档的相关部分。如果你需要进一步深入了解,不要害怕查看GraphQL规范

3.不要担心版本问题

GraphQL的第一条规则是没有真正的版本控制

我们这些使用过REST APIs的人可能会被返回给我们的客户的巨大JSON有效载荷的经验所困扰,不管客户是否真的需要其中的一切。然而,对于GraphQL,重要的是要记住:如果客户端不需要某个特定的数据,它就不会要求它,也不会得到它。

这样做的最终结果是,开发GraphQL模式的过程主要是添加性的。你只是不断地添加新的字段。此外,由于客户端的开发是由源自服务器的模式驱动的,在正常使用中,没有任何工作流程会让较新的客户端与较旧的服务器对话。在最坏的情况下,一个较老的客户端将与一个较新的服务器对话,而且--假设你没有从服务器上删除对任何字段的支持--客户端仍然能够获得它所需要的一切。

简而言之,客户端和服务器通常没有必要通过版本编号方案进行同步(尽管它对于跟踪服务器的模式如何随时间变化很有用)。对于那些罕见的情况,即你觉得你做了一个真正的设计错误,并且确实想删除一个字段,@deprecated指令可以帮助管理这个过程。

4.让客户来驱动你的模式

现在你已经在高层次上学习了一些关于GraphQL的知识,是时候深入进去,开始设计你希望你的模式是什么样的。这可能是令人生畏的,因为很难知道从哪里开始。

我得到的最好的建议是,让客户来推动设计。换句话说,设计一个能够满足特定客户需求的模式。定义在该客户的用户界面上有意义的类型和字段。如果你有多个客户,那就选一个,先做这个。因为你不必担心GraphQL的版本问题,你可以在以后添加新的东西来支持新的客户。

你不应该做的一件事是先发制人地试图孤立地编写一个通用的模式。你可能会听到供应商宣称GraphQL是一种为你的整个组织进行数据建模的方式。也许它是,也许不是。无论如何,如果你是GraphQL的新手,不要尝试这样做。以后可能会出现概括性的机会,如果它们出现了,GraphQL的附加性质意味着它们通常能够愉快地与你想出的最初的客户特定模式并存。但是,如果你过早地尝试概括它,你就有可能弄错它,使客户的生活变得更困难,而不是更容易。

你也不应该让后端系统来决定你在模式中定义的名称和关系。你的GraphQL服务器的工作是在你的客户需要和你的后端拥有的东西之间进行映射。模式是客户与服务器的接口,所以客户的需求应该推动这个模式的设计。服务器应该是能够照顾到血淋淋的细节的东西,而不是客户端。

最后,不要试图一蹴而就地完成它。添加一个字段和一个或两个类型,在服务器上实现它们,把它们连接到客户端,然后再退一步。如果有必要的话,回顾一下文档,以巩固你对你所遇到的任何新概念的理解。对任何可能出现的重复进行重构。然后冲洗和重复。

5.避免外键查询

好了,现在我们可以开始进入细枝末节了。我先说一个我见过的很多人犯的错误,包括我自己。

简而言之,如果你发现自己在Query 类型中添加了做外键查询的字段,你可能做错了什么。例如,设想我们有一个GraphQL服务器,实现了以下模式

type Parent {

如果用户想看某个父类的名字,我们会向服务器发送以下查询

query GetParent($id: ID!) { 

但是,如果用户随后想看那个特定的父辈的孩子的名字呢?你可能会想,你必须在Query 类型中添加一个全新的字段

children(parentId: ID!): [Child!]!

然后像这样查询这个新字段

query GetChildren($parentId: ID!) { 

然而,在Query 类型上的新的children 字段是不必要的,因为原始模式已经有我们需要的一切。我们所要做的就是重新设计我们的查询,通过Query 上现有的parent 字段获得children

query GetChildren($parentId: ID!) { 

有些人看到这一点会感到不安。如果他们要查找一个Parent ,他们至少应该得到它的nameid ?嗯,不是。当你查询一个对象时,你不需要请求任何标量类型的字段。如果你想要的只是一个列表字段,你只需要要求这个字段就可以了。

因此,作为一般规则,在向Query 类型添加新字段之前,我总是建议仔细检查你是否可以利用现有的字段来获得你需要的东西。这在外键关系中尤其如此。

6.对客户保持不透明的ID

这是可选的,但我强烈建议这样做。ID对客户端来说应该是没有任何意义的,除了它是一些可以用来从GraphQL服务器上查找实体的 "东西"。这意味着客户绝对不应该试图解析ID的值。ID也不应该被显示给用户,除非用户需要手动将其输入到外部系统。

我推荐这样做是因为,虽然ID's被序列化为一个字符串,但这个字符串的内部表示可能会随着时间的推移而改变。例如,一个实体可能一开始就被存储在DynamoDB表中,ID被映射为一个键。然而,如果它被转移到一个关系型数据库,这个键的格式可能需要改变。另外,虽然你现在可能对简单的ID感到满意,但你可能希望为将来使用更复杂的全局对象识别模式留有余地。我甚至遇到过这样的情况,由于我所使用的后端系统的限制,我的一些ID实际上不得不被转换为几个不同值的组合,并被序列化为JSON对象。

简而言之,如果客户端对键的内部表示法做了任何假设,但你决定改变这种表示法,那么客户端就必须要更新。如果它把表示法当作不透明的,任何改变都不会影响它。

所以要注意这样的字段。

type SomeType { 

虽然可以说name 总是唯一的,但根据我的经验,这样的字段不会保持很长时间的唯一。因此,我对你这样做没有异议。

type SomeType { id: ID! name: String! }

...即使idname 实际上有相同的值。这是一个值得付出的代价,以保持这种灵活性。

7.使用一个自定义的标量类型来表示日期

在这个问题上,我将直奔主题,因为我对此有强烈的感受。如果你想在你的GraphQL模式中放入日期,使用RFC3339/ISO8601格式的字符串。

我曾经在一个核心后台系统工作过,其中时间被表示为1970年1月1日午夜后的秒数。唯一的问题是,这是在达尔文时区的午夜。因为它只是一个数字,没有任何迹象表明这是事实。每个人都会认为这是UTC,并想知道为什么他们显示给终端用户的日期总是偏离9个半小时。唯一值得安慰的是,达尔文没有夏令时,所以至少你全年都是九个半小时。

因此,无论在什么时区,都不要使用自纪元以来的秒或毫秒。不要使用你自己的字符串格式。不要定义你自己的GraphQLtype 。相反,使用RFC3339/ISO8601字符串。与其他任何机制不同,它是明确的。它也是人类可读的。最后,许多标准库可以解析和格式化它。

如果你真的想提高你的技术水平,使用一个自定义的scalar,将这种格式序列化为一个字符串。如果你使用的是基于graphql-js的服务器,甚至有一个序列化/反序列化器已经可以做到这一点。但无论你做什么,都不要使用RFC3339/ISO8601以外的东西。人生苦短,不能再为这个问题争论和混淆。这是一个已经解决的问题。让我们继续前进。我甚至不知道我们为什么还要讨论这个问题。不管了,我不在乎,闭嘴吧。

8.尽可能多地使用!运算符

好了,现在我已经把日期格式化的问题解决了,让我们继续前进。下一个建议是一个有争议的建议,因为它违背了GraphQL的默认行为。

你可能已经注意到在前面的模式例子中,有很多! 操作符。如果你还不知道,这个操作符规定了一个字段或参数或变量不能有空值。我建议你尽可能多地使用它。

为什么呢?因为冒着说得很明显的风险,如果某个东西可以为空,那么你就得处理它空的情况。最起码,如果你已经表明某些东西可以为空,那么你的GraphQL服务器就不会在运行时为你检查这些值,因为它们会进入和离开服务器--你需要自己做这个检查。

此外,如果你使用代码生成器来生成静态类型,那么GraphQL类型中的一个可忽略的字段将导致相应的生成类型中的一个可忽略的字段。这意味着,如果你使用的静态类型检查器对空值有严格要求(例如,启用了strictNullChecks标志的TypeScript,我一直建议你使用这个标志),那么编译器将迫使你在编译时处理一个值为空的可能性。如果它可能是空的,这很好,但如果它实际上不会是空的,那就是浪费时间了。

Facebook认为,典型的情况是某些东西是可以为空的。我认为这是因为他们想用GraphQL来构建客户端,即使面临数据丢失也能继续运行。然而,你不是Facebook,特别是如果你刚刚开始使用GraphQL。所以,除非你真的面临一个字段可能为空的情况,否则不要费心让它为空。在你的模式中,你会有很多! ,但你在运行时和静态检查你的代码时,会少很多担心。

9.记住,你可以查询多个根字段

这对GraphQL专家来说可能是显而易见的,但我看到它把新手绊倒了。如果你需要从Query 类型中获取两个不同字段的值,记住你可以在一次操作中从该类型中获取多个字段。考虑一下下面的示例模式。

type Cat {

如果你想得到一只有特定ID的猫的名字,你会用这样的方式查询服务器。

query GetCat($id: ID!) { 

但是,如果你想同时得到一只特定的狗的名字,而不需要在各自的网络请求中发送两个单独的查询,那该怎么办?我看到有人认为,要做到这一点,他们将需要在模式中的Query 类型中添加另一个字段,其中该字段的类型专门包含他们想要的信息。比如说。

type Query {
  ...
  catAndDog(catId: ID!, dogId: ID!): CatAndDog!
}
...

type CatAndDog {
  cat: Cat!
  dog: Dog!
}

然后他们会用类似的东西来查询。

query GetCatAndDog($catId: ID!, $dogId: ID!) { 

然而,你不需要一个新的类型,或一个新的字段。相反,你可以在一个单一的查询中请求原来的两个根字段。

query GetCatAndDog($catId: ID!, $dogId: ID!) { 

这样做,你就省去了创建一个新类型的麻烦,也避免了在Query 类型中添加一个新字段。不客气。

10.10.不要害怕使用一个interface

最后一条是我从个人经验中学到的。事实证明,GraphQLinterfaces可以成为对共享字段的实体进行建模的一个好方法。然而,推迟使用它们可能是很诱人的,因为它们一开始看起来有点吓人。然而,重要的是要尽早进入并使用它们,因为从长远来看,它们往往会使你的生活更容易。

例如,在构建一个应用程序时,一个常见的模式是显示一个实体的列表,其中所有的实体都有一个或多个共同的属性。然后,用户可能会选择更详细地查看这些实体中的一个。然而,该实体的细节可能有所不同,这取决于它的类型。为了说明这一点,让我们在前面的提示中扩展我们的模式,在Dog 中增加一个字段,称为numberOfSticksFetched ,在Cat中增加一个字段,称为numberOfMiceCaught

type Dog {

显然,这些新字段是专门针对狗和猫的。狗(一般)不抓老鼠,而且大家都知道,没有一只自尊心强的猫会为你拿棍子。然而,它们都仍然有一个idname 字段。

如果我们想得到一个所有猫和狗的列表,并且只关心在该列表中显示它们的名字,我们可以Query 类型中添加几个字段。

type Query {
  ...
  cats: [Cat!]!
  dogs: [Dog!]!
}
...

然后像这样查询它们

query GetCatsAndDogs { 

然后,我们在客户端将所有的结果卡在一起,并在一个单一的列表中显示。

然而,这样做是重复的,因为我们不得不在两个不同的地方询问idname 。此外,如果我们将来在我们的模式中增加更多的动物类型,有自己的idname 字段,这种重复就会越来越多。

在这一点上,你可能会开始想,我们应该添加一个Animal 类型。但它应该是什么样子呢?我看到有些人开始时是这样的。

type Animal {
  `id: ID`!
  name: String!
  numberOfSticksFetched: Number
  numberOfMiceCaught: Number
}

type Query {
  ...
  animals: \[Animal!\]!
}

其中numberOfSticksFetchednumberOfMiceCaught 是可忽略的,这样我们就可以在运行时分辨出这个动物究竟是猫还是狗。然而,这种建模是有问题的,因为它没有防止它们都是空的,或者都是非空的可能性。

好消息是,GraphQL有一个内置的机制来实现这种多态性,而不是让我们自己去建模:接口。

interface Animal {
  `id: ID`!
  name: String!
}

`type Dog implements Animal {`
  `id: ID`!
  name: String!
  numberOfSticksFetched: Number!
`}`

`type Cat implements Animal {`
  `id: ID`!
  name: String!
  numberOfMiceCaught: Number!
`}`

type Query {
   ...
   animals: \[Animal!\]!
}

在这一点上,它可能只是看起来像一堆额外的类型(我仍然不清楚为什么GraphQL要求你在实现接口的类型中重新声明字段)。然而,在奠定了这个基础之后,你就可以得到一个所有动物的使用列表。

query GetAnimals{ 

注意查询中包含的__typename 字段。这是一个内置的元字段,任何GraphQL服务器都必须给你。它将是一个特定结果的类型的字符串表示。如果你的用户随后想查看某个特定动物的细节,客户端可以使用__typename ,选择运行哪个查询来获得该动物类型的细节。更妙的是,如果我们想添加一种新的动物类型,我们只需添加一个实现了Animal接口的新的type ,而不需要改变其他任何东西。

虽然接口在前期的设置上会有一些工作,但它们是值得努力的。在我设计的许多模式中,我都希望自己能早点咬紧牙关,使用接口。

总结

所以,你有了:我给那些编写第一个GraphQL模式的人的十大建议。

简而言之,首先要确保你使用GraphQL的原因是正确的,而且你要理解关键的概念。接下来,摆脱你在构建版本化REST API时可能存在的先入为主的观念。然后,采取一种渐进的、客户驱动的方法,避免过早的概括或来自后端系统的不适当影响。

如果可能的话,保持你的ID不透明,给自己一点回旋的余地。每当你发现自己在Query 类型中添加了另一个字段时,先退一步问:是否有办法通过利用GraphQL提供的更多东西来实现同样的结果?阅读文档会对此有所帮助。

最后,虽然我会让你对你使用! 操作符的程度做最后的决定,但请记住,每当你不使用ISO字符串来表示一个日期时,就会有一个GraphQL仙女死去。我的意思是说真的,谁会做这样的事情?你真的希望这种事情发生吗?很好,我不这么认为。现在你可以走了,开始研究那个模式了。