你所需要知道的GraphQL,都在这里了...

1,107 阅读7分钟

写在前面

笔者最近要在团队内部分享 GraphQL 相关的知识,顺便以此文章记录一下,希望读者在通读文章后能掌握以下知识:

  • GraphQL 的概念
  • 为什么要使用 GraphQL
  • 如何使用 GraphQL
  • 使用 GraphQL 需要注意的问题

概念

GraphQL 的概念很简短:一种用于 API 的查询语言。先暂时抛开这个概念,我们试想一下一门编程语言做了什么:定义了语法,开发者按照指定语法进行代码编程,然后就能把程序跑起来。同理,GraphQl 是一种 API 的查询语言,它定义了 API 查询的语法规则,开发者可以按照该语法规则开发 API,然后客户端就可以向服务器发起请求来进行 API 查询。

对于 API 查询语言,大部分读者应该能想到 REST,没错,GraphQL 在设计之初就是作为 REST 的替代品出现的,它们都代表了 API 的一种设计规范

虽然名字叫做 GraphQL,但它和数据库没有直接的关系,官方文档 FAQ 还为此特地做出解释。

为什么要用 GraphQL

从 2012 年面世至今,已经有很多大厂在业务中落地了 GraphQL,Facebook自不必说,GitHub 在它们的 API V4 中全面使用了 GraphQL,Airbnb 也已经将其大部分 API 迁移到 GraphQL... 因此,GraphQL 已经经过了业务的验证,所以我们可以放心地使用了。

那么相对于传统的 REST 来说,GraphQL 有什么样的魅力能在这些公司的技术选型之争中胜出呢。

端点

REST 的核心理念是资源,并且讲究一个接口操作单一资源,放到我们的日常业务来看,我们需要请求多个不同的 url 来获取不同的数据。

与之相对的是,GraphQL 通常只有一个入口,我们通过参数描述得到我们想要的资源,这意味着我们只需要更少的网络连接花销即可完成同样的请求。

精准获取数据

不同的终端要展示的信息可能不尽相同,比如 web 端通常需要尽可能地展现所有信息,而对于手机端可能只需要展示关键的概要信息。但是从开发的角度看,我们通常希望一个接口能满足不同端的需求,这就造成了在某些终端的网络资源浪费,因为响应包把无用的数据也返回到客户端了。

面对上面的问题,在使用 REST 的情况下,有两种方案:

  • 为不同终端提供不同的 API
  • 第二是使用一个 API,返回不同终端需要的数据的并集

这两种方案都有各自的问题,第一种方案添加了额外的开发工作量,而第二种方案造成了不同终端下数据的过度获取

使用 GraphQL 可以很好地解决上面的问题,因为控制数据的是应用,而不是服务器。

强大的开发者工具

GraphiQL 是 GraphQL 服务自带的 IDE,开发者不仅可以在里面查看文档,并且在调试的时候可以享受里面的智能代码提示以及错误提示,这给开发者带来了很大的便利。

更多强大的特性

GraphQL 还有很多强大的特性,比如无需划分版本的 API 演进过程,自带的参数校验以及服务端推送等,更多详细的信息请参考官网

如何使用 GraphQL

介绍完 GraphQL 的诸多特性后,正式开始进入使用环节了。

一个完整的 GraphQL 查询一般需要经过三个步骤:描述数据、请求数据和得到结果。

描述数据

type Project {
  name: String
  tagline: String
  contributors: [User]
}

我们一一解析上面这段数据描述代码:

  • type 为关键字,表明接下来将定义一个 GraphQL 对象类型,也就是跟在后面的 Project
  • nametaglinecontributorsProject 类型上的字段,表明在一个操作 Project 类型的 GraphQL 查询中的任何部分,都只能出现 nametaglinecontributors 字段。
  • String 是内置的标量类型之一,我们无法在查询中对标量类型进行次级选择。
  • [User] 表明一个空值或者一个 User 数组,因此所以当你查询 contributors 字段时,总能得到一个空值,或者一个数组,这个组数里面的每个对象都是空值或者 User 类型的。

请求数据

GraphQL schema 有两个特殊类型:Query(数据查询) 和 Mutation(数据变更),它们定义了每一个 GraphQL 查询的入口,这意味着 GraphQL 的每个查询必然是 Query 或着 Mutation 类型下的字段

因此,如果我们想执行以下的查询:

{
  project(name: "GraphQL") {
    tagline
  }
}

则必然会存在以下的 schema 定义:

type Query {
  project(name: String): Project
}

得到结果

响应结果的结构和我们描述的结构完全一致,这使得我们可以按照我们的预期使用数据,极大地提高了客户端的开发效率。

GraphQL 还有很多基本的概念和用法,比如变量、别名、指令和类型系统等,由于篇幅关系,这里不一一介绍,感兴趣的读者可以访问官方文档查看。

客户端实现

GraphQL 客户端实现方案可以帮助开发者更好地进行数据的请求和处理,使开发者可以专注开发 UI。Relay 和 Apollo 是提及比较多的两种客户端解决方案。

  • Relay:Relay 是 Facebook 为其自家移动应用程序所构建的方案,是一个主要满足自家需求的 GraphQL 客户端。

  • Apollo:Apollo 是 GraphQL 社区的开源方案,是各大公司更多选用的一种解决方案。Apollo 提供了从 IDE(Apollo Studio,GraphQL 云服务平台)、客户端(Apollo Client)到服务端(Apollo Server)一整套完善的解决方案以及文档,更详细的信息请访问 apollographql

使用 GraphQL 需要注意的问题

抛开具体场景来说,任何方案都不会是完美的,GraphQL 也不例外。N+1问题 是 GraphQL 最需要重视的问题之一。

N+1 问题

N+1问题 是由 GraphQL 的执行机制引起的:一个 GraphQL 服务是通过定义类型以及类型上的字段来创建的,然后给每个类型的每个字段提供解析函数。

例如,我们通过一个 GraphQL 服务获取用户列表,其类型定义应该是这样的:

type Query {
  users: User[]
}

type User {
  id: ID
  name: String
  addressId: Int
  address: Address
}

type Address {
  id: ID
  province: String
  city: String
}

同时,它的解析函数如下:

function queryUsers(request) {
  // 一次db查询获取用户列表
}

function getAddress(user) {
  // 通过单个 User 对象解析地址信息
  const { addressId } = user;
  // 通过 addressId 查询地址表
}

简单来说就是,当我们解析某一个字段时,只能通过该字段的父级类型对象数据来进行解析。来到上面的例子,在解析 address 字段时,只能通过其父级类型 User 的单个实例对象(假设为)user 来获取,N 个用户就要查询 N 次地址表,再加上第一次从数据批量查询用户列表,整个请求经历了 N+1 次数据库查询。

聪明的你们肯定已经想到了优化的方案了:在 address 字段的解析函数里面收集所有的 addressId,然后通过 in 操作符将所有的 addressId 作为条件进行数据库查询,这样就可以通过一次数据库查询将所有地址查询出来了。

DataLoader 使我们能够实现上面的优化,主要核心点是批处理和缓存。这个仓库的代码很简洁,去除注释后应该不到400行代码,感兴趣的读者可自行查看其实现原理。

总结

至此,关于 GraphQL 的介绍就到这里为止,由于篇幅和水平受限,还有很多特性都一略而过了,欢迎各位在评论区留下你的足迹一起探讨问题。