GraphQL 初探—面向未来 API 及其生态圈
什么是 GraphQL ?第一次看到这个名词未免让人联想到数据库查询语言 SQL 。但本质上,这是两个完全不同的东西, GraphQL 在官方文档里的定义如下:
GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data.
即 GraphQL 既是一个 API 查询语言,也指其服务端实现。但 GraphQL 不只是为了在 API 领域搞个类似数据库的查询语言,它的诞生更涉及到 API 设计的思路转变。
REST 模式的难题
通常,一项新技术的产生都会伴随着两个背景,一个是该技术所在的领域出现了新趋势、二是原有的技术难以应对新趋势。而近几年, API 领域有几个趋势愈发值得关注:
首先是日益增多的移动端应用,和移动端性能本身较低下的矛盾,要求数据加载过程更高效。
再者,要满足客户端和前端快速开发、快速添加特性的需求, API 必须能快速拓展。
第三则是各种不同的前端框架和平台层出不穷,而后端 API 服务面对众多的前端框架、乃至前端和客户端共享 API 的情况,其能否按需提供数据,会影响接口复用度和开发效率。
而现如今在 API 领域被广泛使用的 REST 模式,面对上述愈发复杂的客户端和服务端交互,问题也渐渐浮现:
首先是接口灵活性差。由于设计接口粒度较粗或历史遗留原因,接口中有时会存在当前数据交互不需要的字段,导致取到无用且多余的数据;而另一方面,有时前端需要一份数据,却需要手动访问多个接口才能完整获取。
第二是接口操作流程繁琐,回想下前端获取数据的过程,通常我们要先构造 HTTP 请求,然后接收和解析服务端响应。有时还要对收到的或处理后的数据另作本地数据转储,最后才进行 UI 展示。
第三,随着客户端功能拓展,服务端不断增加接口。这样维护众多接口,不仅服务端维护成本高,此外也不能按需提供数据、阻碍了客户端的快速迭代和拓展。
还有 REST 模式实质上是基于 HTTP 协议的,这虽让其易于被 Web 开发人员理解和上手,但也决定它不能灵活选择网络协议来解决问题。
GraphQL 的解决方案
面对 REST 模式的上述不足, Facebook 提出了他们的解决方案 – GraphQL :
前面提到 GraphQL 既是一个 API 查询语言,也指其服务端实现,所以 GraphQL 本身也由两部分组成,Facebook 将它们分别开源:
- 语言标准: Open Web Foundation Agreement (OWFa) v1.0 协议
- GraphQL.js、客户端工具Relay: MIT 协议
我们来逐条了解下 GraphQL 的特性:
声明式的数据获取
如下图所示,声明式的数据查询带来了接口的精确返回,服务器会按数据查询的格式返回同样结构的 JSON 数据、真正照顾了客户端的灵活性:
另外,这种数据获取方式也带来更简洁的数据查询流程。 GraphQL 认为,客户端只需描述查询结构发起查询,再把服务端响应数据用于 UI 展示即可。中间构造请求和转储数据的操作可以交由 GraphQL 客户端辅助完成。
一个服务仅暴露一个 GraphQL 层
上图是一个 GraphQL 应用的基本架构,其中客户端只和 GraphQL 层进行 API 交互,而 GraphQL 层再往后接入各种数据源。这样一来,只要是数据源有的数据, GraphQL 层都可以让客户端按需获取,不必专门再去定接口了。
传输层无关、数据库技术无关
带来了更灵活的技术栈选择,比如我们可以选择对移动设备友好的协议,将网络传输数据量最小化,实现在网络协议层面优化应用。
GraphQL 接入概览
既然 GraphQL 有诸多优点,那又该如何接入呢?大体上,有三种接入的方式:
直连数据库的GraphQL服务
最为简洁的服务配置,直接操作数据库也能减少中间环节的性能损耗。
集成现有服务的GraphQL层
这种配置适合于旧服务的改造,尤其是在涉及第三方服务时、依然可以通过原有接口进行交互。
直连数据库和集成服务的混合模式
前两种方式的混合:
GraphQL 核心概念浅析
GraphQL 的一大特点便是声明式的 API Schema ,GraphQL 的 Schema 是一个声明式的查询规范(可认为是服务器和客户端间的一个查询协议),它主要由两部分组成:
- 类型系统
- 编写语法:SDL(视图定义语言)
GraphQL 的类型系统包含了各编程语言中通用的一些数据类型,具体可参考规范文档了解。
接下来简单介绍下 GraphQL 的 SDL 语法:
定义 API Schema
自定义类型的定义主要是在服务端完成的,语法如下:
type 类型名 {
字段名: 类型
}
此外, GraphQL 还有 Query, Mutation, Subscription 等特殊的根类型,用于定义 API Schema 。我们可以定义一个用户:
type User {
id: Int!
name: String
}
然后定义几个用于数据操作的 API Schema :
type Query { // 基本查询 Schema
user(id: Int!): User // 传入一个 id ,返回具体用户
}
type Mutation { // 操作数据的 Schema
createUser( // 传入用户名自动创建一个用户
name: String
): User
}
type Subscription { // 监听数据变更的 Schema
userChanged: User
}
数据操作
有了这些定义好的 API Schema ,我们就可以此来发起数据操作了。 GraphQL 的数据操作也分为 Query, Mutation, Subscription 三种类型。简单来讲, Query 就是获取数据的基本查询;Mutation 支持对数据的增、删、改等操作;而 Subscription 则用于监听数据变动、并靠 Websocket 等协议推送变动的消息给订阅方。
基于前面的定义的用户 Schema ,我们可以写出如下的数据操作:
query {
user(id:3) { // 查询用户 id 为3的用户
name
}
}
mutation {
createUser(name: "Tom") { // 新增一个名为 "Tom" 的用户
name
id
}
}
subscription {
userChanged { // 监听用户数据变动
name
id
}
}
上面这些查询,根字段之后的所有内容称为查询的 payload 。服务端会按查询格式,在 data 字段返回 payload 中指定的数据,比如 createUser 这个操作就会返回如下的数据:
{
"data": {
"createUser": {
"name": "Tom",
"id": 9
}
}
}
GraphQL 生态圈
通过 API Schema,我们既可指定 API 功能、同时也能定义客户端如何请求数据。但前面介绍的只是个规范,而这个 GraphQL 的规范又是如何落地实现的呢?接下来会围绕服务端、客户端、调试工具,介绍下 GraphQL 应用开发的 “生态圈”。
服务端实现
在服务端, GraphQL 服务器可用任何可构建 Web 服务器的语言实现。除 JavaScript 之外, Ruby , Python , Scala , Java , Clojure , Go 和 .NET 都有实现供参考。
服务端查询执行的核心算法也很简单:就是查询逐字段遍历,并为各字段执行一个 resolver 以处理数据操作。下图举了一个例子:
最左边为一个 GraphQL 查询,该语句查询了 id 为 'abc' 的作者所有文章的标题和内容。中间一副图展示了每个查询字段对应的数据类型,然后在最右边可看到每个字段的解析过程:首先查询 id 为 'abc' 的作者,再从该作者处获取其所有文章;而由于文章是一个列表,最后我们还要遍历这个列表以获取各文章对应的标题、内容。
这个逐字段解析的流程清晰易懂,但如果服务器只是这么实现的话,就会面临性能问题。见下图的例子,若用户要查询文章列表下各个作者的信息,由于文章列表中可能有大量重复的作者,当处理到同一作者的文章时就要重复查询该作者信息,甚至当“查询作者信息”这操作本身就包含大量子操作的话、对服务器性能的消耗就非常可观:
对这种一个查询触发大量相同的数据操作的问题,一种解决思路是将数据操作改为批量处理。还是用上面的例子,下图中我们把查询作者信息的操作改为存入一个队列,待合适的时机再批量发起查询,这时查询的数量就只是队列里的一个最小子集,避免了重复操作。 Facebook 推出的DataLoder 就是一个这样的数据批量处理和缓存的方案。
上面讨论了 GraphQL 服务端的基本实现思路,而针对 Node.js 的实现,我基于前文示例中的 API Schema 写了一个简单的 Demo ,读者可了解下 GraphQL 的服务端具体是如何实现和使用的。
客户端实现
常见的 GraphQL 客户端库有:
- Relay:Facebook 官方的 GraphQL 客户端,它大大优化了性能,但只能在 Web 上可用
- Apollo:一个开源社区项目,旨在为所有开发平台(Web, 安卓, iOS , React Native 等)构建强大而灵活的 GraphQL 客户端
至于如何使用这两个客户端库,可以参考官方文档,这里不再赘述。而对于 Apollo 的入门,Full-stack React + GraphQL Tutorial 一文提供了深入浅出的示例,建议动手尝试下,构建自己第一个 GraphQL 应用吧。
开发工具
GraphQL 有大量实用的开发工具,基本都是基于 introspection 查询实现的。所谓 introspection 查询,就是指客户端向服务器询问 API Schema 信息的查询。比如,我们可以通过查询 __schema 等元字段来获取完整的类型信息:
query {
__schema {
types {
name // 获取根字段名
fields {
name // 获取字段名
}
}
}
}
有了这样一个查询 Schema 信息的功能,就使得 GraphQL 的文档浏览器,自动补全,代码生成等开发工具非常容易实现。而开发工具中,最有名的就是 GraphiQL 了,其本质上可认为是个 GraphQL 客户端,但配有编辑、自动补全、文档浏览等功能,常用于服务端的调试。
前面我们那个服务端 Demo 也以中间件形式引入了基于 GraphiQL 的调试工具 GraphQL PlayGround 。运行 Demo 后,你可以访问 localhost:3000/playground 试试上面列举的所有查询~
GraphQL 存在的问题
当然, GraphQL 也不是完美无缺的,现在 GraphQL 主要存在安全性和服务端缓存能力两方面的问题。
安全问题
GraphQL 声明式的的数据查询提供了灵活、易拓展的接口;但如果我们发起的一次查询包含了过多的数据操作,那么这一次查询就会给数据服务器的带来巨大的压力,提升了被 DDOS 的风险。
此外,每次发起的查询语句,实质上也反映了查询文档的结构,如果被攻击者截取了我们的请求、拼凑出完整的接口内容,这也不利于接口的安全。
面对查询压力,我们可以通过服务端限流、客户端限流等措施来进行缓解,具体限流的措施可参见这篇文章。
而对于 API 结构公开传输的问题,有人提出一个持久化查询的方案。简单来讲,就是客户端和服务端分别将约定好查询内容转换为查询ID,转而使用查询ID进行查询。这样一来既解决了查询语句公开传输的问题,而只传 ID 还顺便减少了传输的数据量、提升了传输速度。
服务端缓存能力
GraphQL 能让客户端灵活地请求数据,这就样一来客户端请求内容就是不确定的,服务端难以根据同一个连接来维护查询缓存。
关于这个问题,前面提到 Facebook 有一个 DataLoader 的技术,可用于实现查询的批量处理和缓存,但其文档中描述的缓存也只是针对单个请求进行、粒度还是较粗。
总结
GraphQL 作为一个新的 API 标准,通过声明式的数据获取方式,给客户端提供了简洁、灵活、高效的数据查询。适应了移动互联网时代客户端技术的快速发展和需求的快速迭代,是当前 REST 模式的有力竞争者。
同时其活跃的社区和日渐成熟的生态圈也证明了这是一个很有生命力的技术,目前 GraphQL 已被许多的公司( Facebook , GitHub , Twitter 等等)用于生产环境中,相信其未来还有很大的发展前景。
但 GraphQL 自身存在的安全性等问题也不容忽视;此外引入 GraphQL 势必存在学习成本,在 API 设计思想上的变化页还会影响到相应的开发模式、开发流程。所以只有权衡好引入成本和收益,才能让这项新技术用在刀刃上。