GraphQL 入门 (1)
官网,可以理解成翻译官方文档。在下篇中将来一发实战。
介绍
GraphQL 是 API 查询语言,也是一个在服务端根据你定义的类型系统数据来执行查询的运行时。它并不与任何数据库或者存储引擎绑定。通过描述你需要的数据格式,返回对应的 JSON 数据,最大可能地减少多余字段以及请求数量。
查询和更新
字段
例子来一发:
req:
1 | { |
res:
1 | { |
直接描述你要的数据格式,然后返回相应的数据格式。字段可以不仅返回一个 String,也可以是一个对象。下面这个例子,
req:
1 | { |
res:
1 | { |
这个例子, friends 返回了一个数组。这是在 schema 中定义的,后面会说。
参数
req:
1 | { |
res:
1 | { |
在像 REST 这样的系统中,你只能传递参数的一个集合:查询参数和 URL 中的参数。在 GraphQL 中,每一个字段和嵌套的对象都能得到它自己的一系列参数,使得 GraphQL 完全替代了多个 API 请求。你可以在标量上传入参数,在 server 端完成转换,而不用在客户端单独处理,再继续请求下一个 API。如下:
req:
1 | { |
res:
1 | { |
参数可能会有非常多不同的类型。在上面的例子上,我们使用了一个枚举的类型,代表了有限集合的一个(本例中,长度的单位,METER 或者 FOOT)。GraphQL 提出了一些默认的类型,但 GraphQL 服务器能定义自己的类型,只要它们能够被序列化到你的变换格式中。
别名
req:
1 | { |
res:
1 | { |
这个例子中,我们是要查询多个 hero 的数据。由于 js 语法限制,一个对象中不能有同名的 key,因此出现了别名的概念。使用 empireHero/jediHero 来代替 hero。
片段
设想一下这样的场景:在上面的例子中,对 hero 使用了别名。这两个 key 都包含了 name 字段。如果此时需要多个字段,那么会这样写:
1 | { |
一下子就看出来其冗余。为了避免代码重复,引入了片段的概念。它类似于 js 的对象展开符号:…
req:
1 | { |
res:
1 | { |
下面的 fragment 定义也是建立在 schame 上的。
在片段中使用变量
req:
1 | query HeroComparison($first: Int = 3) { |
query 定义了一个变量 first,类型为 Int,默认值为 3。在 fragment 中,friendsConnection 传递了变量 first,值为 $first(即 query 定义的这个同名变量)。这样就实现了在 fragment 中使用变量。
操作名称
下面的例子中,使用 query 作为操作类型, HeroNameAndFriends 作为操作名称。
req:
1 | query HeroNameAndFriends { |
res:
1 | { |
操作类型会是:query,mutaion,subscription 和你要做的操作描述。操作类型是必须的,除非你使用查询的简略语法,也就是不用提供名称或者对于你操作的变量。
操作名称是针对你操作的明确的、有意义的名字。它只在多操作文档中必须。但是鼓励都写上操作名字,方便日志记录即错误追踪。
变量
很明显,对于很多接口我们都是有参数的。而如果把参数拼在请求 req 中的话,手动来拼凑,非常低效。相反地,GraphQL 提供了 query 外的变量。
当我们开始使用变量,需要这 3 件事:
- 替换 query 中的静态内容为 $virableName
- 声明 $virableName 为变量的一个值
- 传递 varibleName: value 字典
req:
1 | query HeroNameAndFriends($episode: Episode) { |
变量定义
在上面例子中,变量定义长得像:($episode: Episode)。列出所有变量,前缀为 $,紧跟着类型,这里是 Episode.
所有声明的变量都必须是标量、枚举值或者输入的对象类型。所以如果你想要传递一个复杂的对象给一个字段,你需要知道服务端能匹配的输入类型。详情看 schema。
变量定义可能是可选的,也可能必须。在上面例子,引入在 Episode 类型后没有!感叹号,它就是可选的。否则,必须提供。
默认变量
在类型后可以直接接上: = defaultValue。如下:
req:
1 | query HeroNameAndFriends($episode: Episode = JEDI) { |
指令
有时候我们会需要通过变量来决定 query 的结构。比如,有一个 UI 组件,有一个总结和详情的 view,其中一个包含了更多的字段。
req:
1 | query Hero($episode: Episode, $withFriends: Boolean!) { |
res:
1 | { |
如果把 withFriends 改为 true ,结果就会包含 withFriends 字段。
GraphQL 的核心包括两个指令,任何对其的实现都必须支持这两个:
- @include (if: Boolean) 当 argument 为 true 时包含该字段
- @skip (if: Boolean) 当为 true 时跳过该字段。跟上面刚好相反。
更新
许多关于 GraphQL 的讨论关注在数据获取,但也需要一种方式去修改服务端的数据。
在 REST 中,任何请求都可能对服务器造成一些副作用,但传统来说,不要使用 GET 请求来修改数据。 GraphQL 也是类似的 — 技术上来说,任何查询都被实现去造成数据的写入。然而,最好的方式是通过明确的更新来写入数据。
像 query 一样,如果更新字段返回了一个一个对象类型,你可以请求嵌套的字段。在一次更新后或者新的状态,这点非常有用。举个梨子:
req:
1 | mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { |
res:
1 | { |
这样就把传统的更新后再查询变成了,更新同时获取新的值,变为一次请求。
review 变量不是一个标量,而是一个 input object type 输入对象类型。同样是在 Schema 中定义。
更新多个字段
一次更新可以包含多个字段,就像查询一样。不过有个重要的区别是:
查询字段是并且执行,更新字段却是 串行 执行。一个完了,下一个才开始。
这表明如果我们在一个请求中发送了两个 incrementCredits 更新,第一个结束后,第二个才会开始,确保了不会存在竞态问题。
内联片段
像其他许多的类型系统一样,GraphQL 的 schema 也能够定义接口和联合类型。
如果你请求了一个返回一个接口或者联合类型的字段,你需要使用内联片段去访问底层具体类型的数据。举个梨子:
req:
1 | query HeroForEpisode($ep: Episode!) { |
res:
1 | { |
在这个请求中, hero 字段返回了 Character 类型,它可能是 Human 或者 Droid ,取决于 episode 参数。在这种情况下,你只能访问 Character 上存在的字段,比如 name 。
请求混合类型的字段,你需要用一个类型判断来使用 内联片段 。因为第一个参数被标记为 …on Droid, 字段 primaryFunction 只会在 Character 返回了 hero 是一个 Dorid 才会被执行。同样的 height 对于 Human 类型。
在内联片段中也可以使用具名片段,即:
1 | query HeroForEpisode($ep: Episode!) { |
元字段
鉴于这种情况:你不知道你会从 GraphQL 服务中返回什么类型的数据,你需要确定在客户端中怎么处理这些数据。GraphQL 允许你是用 __typename , 一种元字段,可以写在 query 的任何地方。
req:
1 | { |
res:
1 | { |
在上面的 query 中, search 返回了一个联合类型,可以是 3 种类型之一。如果不使用 __typename 字段,就不可能区分这几种类型。
GraphQL 提供了不多的源字段,剩下的在 Introspection 系统中说明。