写在前面
笔者最近要在团队内部分享 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
。name
、tagline
和contributors
是Project
类型上的字段,表明在一个操作Project
类型的 GraphQL 查询中的任何部分,都只能出现name
、tagline
和contributors
字段。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 的介绍就到这里为止,由于篇幅和水平受限,还有很多特性都一略而过了,欢迎各位在评论区留下你的足迹一起探讨问题。