什么是 GraphQL?
了解其他API技术面临的挑战,以及GraphQL是如何解决这些问题的。
什么是 GraphQL?
GraphQL 已迅速成为全栈、前端和后端开发人员工具箱中的重要工具。采用 GraphQL 的公司称赞其高效与易用性,而个人开发者则选择它来提升开发效率。
本课程将教会我们如何从零开始使用 GraphQL 构建一个完整的应用程序。我们将探索如何实现带有数据库的后端、React 网页前端、身份认证以及实时事件等功能。
接下来我们会深入探讨实际和具体的技术细节,但在那之前,我们需要先建立一些基本的理论基础。
REST API
在深入了解 GraphQL 是什么以及如何使用它之前,我们需要了解它的主要替代方案,以及为何探索其他选项是合理的。
在 GraphQL 出现之前,REST 是构建 API 最常见的软件架构之一。简而言之,REST 是一套用于组织应用程序 API 的约定。它基于两个核心概念:资源 和 方法。
资源是我们系统中的任何实体,例如一篇博客文章或一条用户评论。每个资源都有一个唯一的标识符,比如 ID 为 12 的书籍对应 books/12。
我们可以使用可用的方法与资源进行交互。最常用的几种方法包括:
- GET:用于获取数据。
- POST:用于创建新资源。
- PUT:用于更新现有资源。
- DELETE:用于删除资源。
假设我们正在开发一个类似于 Medium 的博客应用。在这个应用中,我们会涉及用户、博客文章和评论等资源。我们的应用需要能够创建、读取和更新这些资源。
要表示一个用户,我们可以有一个名为 /users/1 的资源,代表 ID 为 1 的用户。我们可以发送如下 HTTP 请求:
1// 获取 ID 为 1 的用户 2GET https://blog.com/users/1 3 4// 更新 ID 为 1 的用户 5PUT https://blog.com/users/1
我们也会为博客文章设置相应的资源:
1// 获取所有博客文章 2GET https://blog.com/posts 3 4// 获取 ID 为 1 的博客文章 5GET https://blog.com/posts/1 6 7// 获取 ID 为 4 的博客文章的评论 8GET https://blog.com/posts/4/comments
REST API 在业界被广泛使用。许多应用通过 REST API 向前端应用和第三方客户端暴露数据与操作。
REST API 的问题
尽管 REST API 被广泛采用,但它也存在一些显著挑战。为了说明这一点,我们来看一个为博客应用实现特定功能的例子:我们需要开发一个查看单篇博客文章的页面,该页面应包含文章正文、评论区以及作者姓名等附加信息。
要实现这个功能,我们需要获取以下几类数据:文章本身的信息、文章作者的信息,以及评论列表。
首先,我们发送请求获取所需显示的文章数据:
1GET /posts/123 2 3{ 4 "id": "123", 5 "name": "REST vs GraphQL", 6 "body": "...", 7 "authorIds": ["1", "2"] 8}
这只是一个开始,但我们发现只返回了两位作者的 ID。由于我们有独立的用户信息接口,因此还需要再发两条请求:
1GET /users/1 2 3{ 4 "id": 1, 5 "name": "joe" 6} 7 8GET /users/2 9 10{ 11 "id": 2, 12 "name": "peter42" 13}
现在我们需要获取评论列表,这又需要一次额外请求:
1GET /posts/123/comments 2 3[ 4 { 5 "id": 1, 6 "authorId": 3, 7 "text": "Good post!" 8 }, 9 { 10 "id": 1, 11 "authorId": 4, 12 "text": "Could elaborate on..." 13 } 14]
这次我们拿到了评论数据,但仍然需要获取评论中提到的用户信息。有多少个不同的用户出现在评论中,我们就需要发出多少次额外请求:
1GET /users/3 2GET /users/4 3...
在这个例子中,仅展示一页内容就需要至少发起五次请求。一个明显的问题是客户端必须发送大量请求。用户必须等待应用从服务器逐个获取不同对象,这在慢速网络环境下会造成明显延迟。
这个问题被称为 数据不足(under-fetching),因为每次请求返回的数据少于客户端所需,导致必须发送更多请求才能获取完整信息。
更令人意外的是第二个问题:我们可能并不需要 API 返回的所有数据。现代应用需适配多种设备,从智能手表到桌面应用和智能电视。每种设备屏幕尺寸和输入方式(手指、触控笔、鼠标等)都不同,所需展示的数据也各不相同。例如,在桌面端,我们可以利用更大的屏幕展示每位作者的简介和其他作品;而在移动端,我们只需显示作者姓名即可。
如果我们的 REST API 提供一个通用端点,返回每个资源的所有可用信息,那么就可能向特定客户端返回远超其需求的数据。这种现象称为 数据过度获取(over-fetching),即客户端接收到了实际不会使用的多余信息。
我们举的例子相对简单。对于更复杂的页面,这一过程会更加繁琐,所需 API 调用次数也可能大幅增加。
常见解决方案
这些问题并非新出现,开发者早已意识到它们的存在。
一种常见解决方案是“面向前端的后端”(BFF)模式,即不再开发通用的 REST API,而是为特定 UI 功能设计专用接口。例如,我们可以创建一个端点,一次性返回显示单篇博客文章所需的所有信息:
1GET /post_page_all_info/123 2 3{ 4 "id": 123, 5 "name": "REST vs GraphQL", 6 "body": "...", 7 "authors": [ 8 { 9 "id": 1, 10 "name": "joe" 11 }, 12 { 13 "id": 2, 14 "name": "peter42" 15 } 16 ], 17 "comments": [ 18 { 19 "id": 1, 20 "author": { 21 "id": 3, 22 "name": "alan" 23 }, 24 "text": "Good post!" 25 }, 26 ... 27 ] 28}
这种方式可以通过一次请求获取全部所需数据。但如果需要实现另一个页面,且需要的是同一数据的不同子集,则必须创建新的端点。
另一个问题是,即使每个使用场景只有一个端点,仍可能存在数据过度获取的问题。为解决此问题,我们可能需要进一步细分端点,分别为移动设备、平板、笔记本电脑等不同终端提供定制化的数据子集。例如:
1// 为移动应用获取最少的数据 2GET /post_page_all_info_for_mobile/1 3 4// 为大屏移动设备获取稍多一点的数据 5GET /post_page_all_info_for_big_mobile/1 6 7// 为平板设备获取更多数据 8GET /post_page_all_info_for_tablet/1
可以想象,如果应用稍微复杂一些,维护这么多不同端点将变得极为繁琐,从组织管理角度看也难以扩展。我们需要一支庞大的团队来持续维护这些 API,不断添加字段并支持新的使用场景。
而这正是 GraphQL 可以帮助解决的问题。
构建 API 的另一种方式
现在终于可以回答关键问题:“什么是 GraphQL?”更重要的是,它如何帮助我们构建更好的应用?
GraphQL 的第一个核心特征就在名字中体现:它不再围绕“资源”和“方法”来设计 API,而是迫使我们把数据看作一个相互连接的对象网络(即图结构)。每个对象与其他对象相连,这些对象又连接到其他对象。每个 GraphQL 请求都可以获取该图的一部分数据。例如,以下是我们的博客应用中可能存在的部分数据图结构:
注意:有一种常见误解认为使用 GraphQL 必须搭配图数据库(如 Neo4j),这是不必要的。在实现 GraphQL API 时,我们可以使用任何类型的数据源。
与 REST 不同,客户端使用 GraphQL 查询数据的方式完全不同。GraphQL 只暴露一个单一端点,所有查询都发送到这个端点。
服务器如何知道返回哪些数据?很简单!客户端通过指定需要哪些对象和字段来告诉服务器。
GraphQL 服务器可以使用任意数据源(如数据库或微服务)来处理客户端的查询。
借助 GraphQL API,我们可以完成三项关键任务:
- 查询数据。
- 使用变更(mutations)更新数据。
- 订阅来自后端的实时更新。
这些内容我们将在课程后续部分详细讲解。但在此之前,先来看看如何查询数据。例如,如果我们想根据 ID 获取某篇博客文章的名称及其作者列表,查询语句可能如下:
1{ 2 post(id: 123) { 3 id 4 name 5 authors { 6 id 7 name 8 } 9 } 10}
我们稍后会深入解析,但这段查询并不难理解。客户端请求 ID 为 123 的文章数据,并明确要求返回三个字段:id、name,以及该文章的作者列表。对于每个作者对象,还要求返回其 id 和 name。
该查询的响应可能如下:
1{ 2 "id": 123, 3 "name": "REST vs GraphQL", 4 "authors": [ 5 { 6 "id": 1, 7 "name": "joe" 8 }, 9 { 10 "id": 2, 11 "name": "peter42" 12 } 13 ] 14}
请注意,这与 REST API 大不相同。GraphQL 不提供固定的端点集合,而是允许客户端灵活构造查询,精准获取特定使用场景所需的数据。这为开发者提供了极大的灵活性,可以根据应用的具体需求调整查询逻辑,同时彻底解决了之前提到的数据过度获取问题。
不再由后端开发者定义 API 返回数据的结构,而是每个客户端应用在每次请求时自行描述所需的数据。
一个关键点是:GraphQL 查询明确定义了响应的结构。响应结果中只会包含客户端请求的字段。
这也是 GraphQL 的一大优势:客户端可以确信返回的数据格式是预定义好的。这与 REST 完全不同,因为在 REST 中,客户端无法保证返回结果的结构。
远不止是一种查询语言
此时你可能会觉得,GraphQL 的最大好处在于能实现更高效的 API。虽然这一点非常重要,但这并非唯一优势。GraphQL 社区已开发出一系列工具,极大简化了前端开发工作。
现代客户端应用非常复杂,需要同步状态、协调 API 调用、对数据进行去规范化处理等。真正用于构建 UI 的时间只是开发周期的一小部分。
这时,GraphQL 客户端库的优势便凸显出来。它们能让应用更高效地获取数据,并大大简化客户端的状态管理和与 API 的交互。稍后我们会看到,仅用几行 GraphQL 代码,就能替代大量原本复杂的状态管理逻辑。
GraphQL 的简要历史
GraphQL 最初于 2012 年由 Facebook 创建。当时,像许多其他公司一样,Facebook 也面临我们前面描述的类似问题。这些问题在 Facebook 工程师开始大力拓展移动设备支持并开发新闻动态功能时变得更加突出。
Facebook 已经拥有用于查询新闻动态数据的现有 API,但工程师很快发现使用这些接口存在问题。新闻动态前端需要从服务器获取大量相关对象,如时间线帖子、用户信息、评论等。在移动端加载单页时,多次请求导致延迟极高,有时甚至高达 45 秒。
Facebook 工程师尝试了多种方法来减少往返次数和避免数据过度获取。他们希望减少请求次数,甚至考虑使用 FQL(Facebook 查询语言,一种类似 SQL 的查询语言)来让客户端指定所需数据。但这种方法难以维护,且要求前端编写极其复杂的查询。
最终,他们提出了一种更简洁、足够灵活的查询语言,能够覆盖 Facebook 所需的各种查询场景。它不像 SQL 那样复杂,也更容易维护。这种语言后来演变为 GraphQL。
经过几年内部使用后,Facebook 在 2015 年的 React 大会上首次公开介绍 GraphQL,并开源了 GraphQL 客户端和服务器的实现,从而推动了 GraphQL 在其他公司的广泛应用。
Facebook 并非唯一一家试图解决 API 问题的公司。Coursera 和 Netflix 等企业也在开发更灵活的查询语言。但最终,它们也都转向了 GraphQL,众多其他公司也随之跟进。
GraphQL 的采用情况
尽管 GraphQL 起源于 Facebook 项目,但它目前已不再由该公司独占。Facebook 编写了 GraphQL 规范,促成了一个丰富的生态系统,包括各类客户端库、服务端库以及其他工具。自 2018 年起,该项目由 Linux 基金会托管。
GraphQL 最初是在 React 大会上提出的,如今几乎可与任何主流 Web 框架(如 Angular、Vue 等)或平台(Android、iOS 等)集成。它也不局限于 JavaScript,几乎所有主流编程语言都有对应的客户端和服务器端库。
GraphQL 在业界已被广泛采纳,被 Airbnb、Shopify、Lyft、Pinterest 等公司所使用。
总结
本章内容丰富,我们可以归纳出几个核心要点:
- REST API 可能存在数据不足或数据过度获取的问题。
- 解决这些问题的常见方法往往难以实现且难以扩展。
- GraphQL 是 REST API 的一种新替代方案,能有效解决数据不足和数据过度获取问题。
- GraphQL 允许客户端发送针对特定使用场景量身定制的查询。
- 围绕 GraphQL 已发展出大量工具,尤其是客户端库,极大简化了前端开发工作。