本文翻译自《React 学习之道》 原作者 Robin 的一片文章 https://www.robinwieruch.de/why-graphql-advantages-disadvantages-alternatives/ 。
这篇包含两个部分,本文是它的第一部分。
Part 2:为什么选用 Apollo:它的优点、缺点和备选方案
当谈论到客户端与服务端之间的网络请求时,REST 绝对是连接两者的方案选型中最流行的选择。在 REST 中,所有概念都是在可以通过 URL 可访问的资源这个概念周围演化而来的。你可以通过一个 HTTP GET 请求读取一个资源,通过一个 HTTP POST 请求创建一个资源,或者通过 HTTP PUT 和 HTTP DELETE 更新或删除一个资源。这些被称为 CRUD(Create、Read、Update、Delete)操作。资源可以是任何实体,比如作者(authors)、文章(articles)或者用户(users)。使用 REST 时,传输数据的格式并没有定论,但是多数人会使用 JSON 作为传输数据媒介。最终,REST 使应用之间能直接通过原生的 HTTP URL 以及 HTTP 方法进行数据沟通。
// 一个 RESTful 请求
GET https://api.domain.com/authors/7
// JSON 数据的响应
{
"id": "7",
"name": "Robin Wieruch",
"avatarUrl": "https://domain.com/authors/7",
"firstName": "Robin",
"lastName": "Wieruch"
}
即便 REST 是现在普遍的选择,但最近几年出现了另外一套由 Facebook 提出的技术方案:被称为 GraphQL。下面会给你一个 GraphQL 的简单介绍:它的优点和缺点,以及是否有一些备选方案。
什么是 GraphQL?
在我们谈论 GraphQL 的优点和缺点前,我们先回答下这个问题:什么是 GraphQL?GraphQL是由 Facebook 在 2012 年创立的一门开源查询语言。在它开源之前,Facebook 就已经在内部移动端应用中使用过。为什么选用移动应用?GraphQL 作为通用的 REST 架构的替代方案而被开发出来,它允许客户端只请求其需要的数据——不多也不少,一切在客户端的主导下。在一个 RESTful 架构下,因为后端开发人员定义在各个 URL 的资源上返回的数据,而不是前端开发人员来提出数据需求,使得按需获取数据会非常困难。经常前端需要请求一个资源中所有的信息,即便只需要其中的一部分数据。这个问题被称之为过度获取(overfetching)。最恶劣的场景下,一个客户端应用不得不请求多个而不是一个资源,这通常会发起多个网络请求。这不仅会造成过度获取的问题,也会造成瀑布式的网络请求(waterfall network requests)。那么将像 GraphQL 之类的查询语言,不仅在服务端程使用,也应用到客户端的话,客户端来决定需要什么数据,这样只需要发送一个请求到服务端。在 Facebook 的 GraphQL 移动端开发场景下,这极大地减少了忘了请求,因为 GraphQL 一次只需要发起一个请求,并且传输中数据数量也减少了。
Facebook 开源了 GraphQL 标准和其 JavaScript 版本的实现。后来主要编程语言也实现了标准。此外,GraphQL 周边的生态不仅仅水平上扩展了不同语言的实现,并且还出现了在 GraphQL 基础上实现了类库(比如 Apollo 和 Relay)。
一个 GraphQL 操作可以是一个查询(query(读操作))、修改(mutation(写操作))以及订阅(subscription(持续读操作))。这些操作中每一种都只是根据 GraphQL 标准构造的一段字符串而已。一旦一个 GraphQL 操作从前端应用到达后端应用,首先会在后端解释整个 GraphQL schema,然后再为前端解析相关的数据。GraphQL 并没有要求网络层选型(通常是 HTTP),也没有要求传输数据格式(通常是 JSON)。甚至没有要求应用架构(通常是前后端分离架构)。它只是一个查询语言。
// GraphQL 查询
author(id: "7") {
id
name
avatarUrl
articles(limit: 2) {
name
urlSlug
}
}
// GraphQL 查询结果
{
"data": {
"author": {
"id": "7",
"name": "Robin Wieruch",
"avatarUrl": "https://domain.com/authors/7",
"articles": [
{
"name": "The Road to learn React",
"urlSlug": "the-road-to-learn-react"
},
{
"name": "React Testing Tutorial",
"urlSlug": "react-testing-tutorial"
}
]
}
}
}
如你所见,一个查询请求了多个资源(作者(author)、文章(article)),在 GraphQL 中被称为字段(fileds),及时 GraphQL scheme 提供了更多是数据(比如文章中的描述(description)和发布时间(releaseDate) ),我们只会拿到这些字段的一个子集(文章中的名称(name)和 urlSlug)。相对地,在 RESTful 架构中,需要至少两个连续请求分别获取作者实体和它的文章,GraphQL 只需要一个查询就可以做到。此外,查询只需要选择必要的字段即可,而不是整个数据实体。
这就是 GraphQL 的基本介绍。服务端提供了定义了可用数据的层级关系和类型的 GraphQL schema,客户端只用查询其需要的数据即可。
GraphQL 的优点
接下来会列举在应用中使用 GraphQL 的主要优点。
声明式地数据获取
如之前看到的那样,GraphQL 在使用查询语句式,使用声明式的方式获取数据。客户端在一个查询请求中,选择需要的数据和相关的字段实体。客户端根据其 UI 来决定需要的字段。你可以说这是 UI 驱动地数据获取。比方说,Airbnb 使用 GraphQL的例子,在 Airbnb 中的一个搜索界面,经常需要搜索房屋的住房体验和其他相关的一些信息,为了能在一个请求中检索所有的数据,一个 GraphQL 查询会根据 UI 选择数据中的一部分达到完美的匹配。毕竟,GraphQL 提供了极佳的关注点分离方式:客户端知道它需要什么数据,服务端知道数据的结构,以及如何从一些数据源(比如数据库、微服务、第三方 API)中拉取数据。
在 GraphQL 中没有过度获取
使用 GraphQL 不会存在过度获取的现象。使用与 Web 客户端公用的一个 RESTful API,一个移动客户端很可能会获取过多的数据,但是使用同样的 GraphQL API,移动客户端可以选择和 Web 客户端不同的数据字段。因此移动客户端能减少获取的信息,因为相对于 Web 应用的更大的屏幕,小屏幕上可能显示不了那么多信息。GraphQL 通过最开始按客户端需求选择数据,减少了传输数据的大小。
在 React、Angular、Node 和 Co 中的 GraphQL
GraphQL 并不只让 React 的开发者激动。即便 Facebook 只展示了一个使用 React 的客户端程序,但是它和任何前端或者后端的解决方案是解耦的,无关的。GraphQL 的相关实现是用 JavaScript 写的,因此 GraphQL 可以用在 Angular、Vue、Express、Hapi、Koa 以及任何其他客户端或者服务端的 JavaScript 类库上,并且这还只是在 JavaScript 的生态中。GraphQL 模仿了让 REST 这么流行的一个特点:一个两个实体(比如服务端和客户端)语言无关的接口(查询语言)。这样你可以在任意编程语言中通过使用一个 GraphQL 标准的实现使用 GraphQL 了。
谁在使用 GraphQL?
自从 2012 年,在 GraphQL 开源前,Facebook 就开始使用 GraphQL了,它是驱动 GraphQL 规范化和 JavaScript 参考实现背后的公司。所以开始使用 GraphQL,你就已经站在他们的肩膀之上了。其他著名的公司也在他们的应用中使用着 GraphQL。因为它们应用中也有着庞大的需求,它们投身于 GraphQL 生态。因此,你不是唯一站在 Facebook 肩膀上的人,此外还有这些公司:
当 GraphQL 被 Facebook 开源和开发出来后,其他公司在他们的应用中遇到类似的问题。这就是为什么 Netflix 会退出 [Falcor](https://github.com/Netflix/falcor),看起来是 GraphQL 的一个备选方案。这仅展示了现代应用需求着如 GraphQL 和 Falcor 这样的解决方案。
单一数据源(Single Source of Truth)
在 GraphQL 应用中存在者单一数据源:GraphQL schema。它提供了一个所有可用数据检索的源头。鉴于 GraphQL 的 schema 通常会在服务端定义,客户端可以基于 schema 读取(query)和写入(mutation)数据。因此,服务端提供了所有可用的信息,客户端只需要执行 GraphQL 查询获取部分数据,或者通过 GraphQL 修改变更部分数据。
GraphQL 拥抱趋势
GraphQL 适应了现在应用构建的变化趋势。你可能只有一个后端应用,但是可能会有多个依赖同一个后端应用的客户端(web 端、移动端、智能手表等等...)。因此 GraphQL 不仅能在前后端进行沟通,也能满足每一个客户端的具体要求(比如网络使用的要求、数据嵌套的要求、按需获取数据的要求),而不需要为每一个客户端定制不同的 API。
另外一方面,在服务端,可能不止一个后端应用,而是一个微服务集群来提供各自具体的功能。这简直是 GraphQL 的完美使用场景,它将所有的功能编织汇总到一个 GraphQL schema 汇总。
拼接 GraphQL Schema
拼接 Schema 使得多个 schema 可以聚合成一个。什么时候你需要考虑这个?考虑一下后端的微服务架构。每个微服务处理特定域的业务逻辑和数据。因此,每个微服务都可以定义自己的GraphQL架构。之后,使用 Schema 拼接将所有 Schema 聚合到一个可以被客户端访问的 Schema 中。最终,每个微服务都可以拥有自己的 GraphQL 端点,而一个 GraphQL API网关将所有 schema 合并到一个全局 schema 中,以便使得客户端可以使用。
GraphQL 自省(Introspection)
GraphQL 自省允许通过 GraphQL API 检索 GraphQL schema。因为 schema 包含了包含了 GraphQL API 可以获得的所有数据信息,本身就是一份完美的自动生成的 API 文档。不仅仅是 API 的文档,也允许客户端通过mock GraphQL 的 schema 达到测试的目的,或者使用 schema 拼接的接口检索多个微服务的 schema。
强类型的 GraphQL
GraphQL 是一门强类型的查询语言,因为它是通过 GraphQL Schema Definition Language(SDL)书写的。因为有了强类型,它就拥有了强类型编程语言一样的好处:更不容出错、可以在编辑期验证并且支持编辑器智能补全和验证相关的集成。
GraphQL 版本化
在 GraphQL 中没有 API 版本的说法。在 REST 中,通常会提供一个 API 的多个版本(比如 api.domain.com/v1/、api.domain.com/v2/),因为随着时间过去,可能资源的结构也会发生变化。在 GraphQL 中,API 废弃可以做到字段级别,因此当一个客户端减少到一个废弃的字段,会得到一个废弃相关的警告。当没有客户端再使用这个废弃知道后,就可以从 schema 汇总移除这个字段了。这让一个 GraphQL API 不需要使用版本化的方式来演进。
成长中的 GraphQL 生态
GraphQL 的生态正在发展壮大。不仅仅是 GraphQL 天然的强类型特性适宜集成编辑器和 IDE 的演进,GraphQL 相关的应用也在演进。你可能记得在处理 REST API 的时候的 Postman,现在有 GraphiQL 或者 GraphQL Playground可以调试你的应用。你也可以找到如 Gatsby.js 这样的使用 GraphQL 的 React 静态页面生成器。比如,使用 Gatsby.js 你可以在构建时期通过一个 GraphQL 来提供你的博客内容来源。你可能还听说过内容管理系统(CMS)(比如 GraphCMS)通过 GraphQL API 来提供(博客)内容。不仅在技术领域有所演进,这还有很多 GraphQL 相关 大会、聚会和社区不断涌现,并且也可以通过一些 newsletters 和 podcast 了解到 GraphQL。
我应该全部投入到 GraphQL 么?
采用 GraphQL 并不需要将现有技术栈全部一步推翻。如果你计划从一个单体后端应用迁移到一个微服务架构上去,正是一个绝好的时机为新的微服务引入 GraphQL API。当有多个微服务时,你的团队可以通过 schema 拼接的方式引入一个 GraphQL 网关(gateway)。不过 API 网关并不是微服务中才能使用的方式,单体 REST 应用也可以。你可以通过将所有现有的 API 通过一个 API 网关不断一步一步汇集到一起,逐步完成到 GraphQL 的迁移。
GraphQL 的缺点
下面的话题展示了使用 GraphQL 的一些不足
GraphQL 查询的复杂性
人们经常错误地认为 GraphQL 就是在后端替代了数据库。并不是这样的,GraphQL 仅仅是一个查询语言。在服务端,一个查询需要解析数据,因此一个 GraphQL 相关实现常常需要执行数据库访问,但 GraphQL 其实不关心这些。还有,GraphQL 在你需要在一个查询中获取多个字段(作者、文章、评论)的时候,它对性能瓶颈没有任何帮助。无论使用 RESTful 架构还是 GraphQL,不同资源/字段仍然需要从一个数据源去获取。
因此当一个客户端需要一次查询很多嵌套字段时,前端开发通常不能很清楚他正在通过服务端访问不同的数据库获取过多的数据。这需要一种机制(比如最深查询深度、查询复杂度权重、避免递归、持久化查询)来制止来自客户端的(性能)昂贵的查询。
查询频率限制
另一个问题是频率限制,在 REST 中,可以简单的声明”一天之中,我们只允许请求这么多资源“,在一个独立的 GraphQL 操作中很难做到这一点,因为任何操作的开销都可以是廉价的或者昂贵的。这就是那些有着公共 GraphQL API 的公司提出的特定速率限制计算,通常可以归结为前面提到的最大查询深度和查询复杂度权重问题。
GraphQL 缓存
一个简单缓存,相比 REST,在 GraphQL 中实现会变得极其复杂。在 REST 中你通过 URL 访问资源,因此你可以在资源级别实现缓存,因为资源使用 URL 作为其标识符。在 GraphQL 中就复杂了,因为即便它操作的是同一个实体,每个查询都各不相同。比如,一个查询中,你可能只会请求一个作者的名字,但是在另外一次查询中你可能也想知道他的电子邮箱地址。这就需要你有一个更加健全的机制中来确保字段级别的缓存,实现起来并不简单。不过,多数基于 GraphQL 构建的类库都提供了开箱即用的缓存机制。
为什么不用 REST 呢?
GraphQL 是通常用来连接客户端和服务端的 RESTful 架构的替代方案。在前面的内容中,你已经多次听到 REST 了,那么什么是使用 GraphQL 而不是 RESTful ,显而易见的好处呢?
因为 REST 提出通过 URL 来标识资源,最终常常会出现低效的连续请求。比方说,最开始你通过 id 来定位一个作者实体,然后你通过作者的 id 获取的某个信息来请求他所有的文章。在 GraphQL 中只需要一个请求就能办到,这是更加效率的。更进一步而言,如果你想只想获取作者的所有文章数据,而不关心作者的信息,GraphQL 允许只你选择你需要的信息。在 REST 中,你需要先获取作者的所有实体信息,即使你值关心被这个作者写的文章而已。因为过度获取这个问题,只有在使用 REST 才会出现,而 GraphQL 就不会。
现在客户端应用不适合 RESTful 的服务端应用了。比方说,在 Airbnb 平台上,获取搜索结果,它为你展示了房屋、住房体验以及其他相关信息。住房和住房体验在各自的 RESTful 资源中,那么你需要支持多个网络请求。当使用 GraphQL API,你只需要在一个 GraphQL 查询中一起获取所有需要的实体(比如住房和住房体验)或者嵌套的其他相关信息(比如作者的文章信息)。
最终 GraphQL 将数据有服务端主导返回什么数据变成了客户端决定需要什么什么。这就是一开始 GraphQL 被发明的原因,因为在 Facebook 的一个移动应用和他们的 web 应用需要的数据是不一样的。
总之,仍然有些场景下使用 REST 来沟通客户端和服务端是有价值的途径,通常应用是资源驱动,也不需要像 GraphQL 这些的查询语言提供的灵活能力。然而,我还是推荐你尝试使用 GraphQL 来开发你的下一个客户/服务端架构。
GraphQL 的替代方案
显然 REST 是最流行的 GraphQL 替代方案。最近这些年里,通常使用 RESTful 架构来沟通客户端和服务端,比起其他网络技术,如 RPC 和 SOAP,因为它使用了 HTTP 的原本的特特性,而其他协议(比如 SOAP)试图在此基础上构建起自己的解决方案。
由 Netflix 开发的 Falcor 是之前有提到的另外一个替代方案。它在 GraphQL 被 Facebook 创建的同一时间被开发出来。Netflix 遇到了同样的问题,并且开源了它的解决方案。在 Falcor 周围并没有什么关注,可能是因为 GraphQL 变得非常流行的原因吧,但是在 Netflix 的开发者在过去的时间里,用了很大的经历开发,因此,这个方案值是得深入研究的。
对于采用 GraphQL 而不是实现另外一套 RESTful 架构,已经有很多理由了。它有很多优点,也非常适合现代软件架构。所以试着学习并且用它构建一些应用吧。