用 GraphQL 还是不用?优点和缺点

2025-06-09

用 GraphQL 还是不用?优点和缺点

所以你想知道 GraphQL 是否适合你的项目吗?无论好坏,使用 GraphQL 都可能是一个重要的决定。如果你从未在复杂的项目中使用过 GraphQL,你怎么知道以后会后悔,还是会因为做出了正确的选择而感到庆幸呢?

诸如“GraphQL 是 REST 的继承者”“GraphQL 对大多数项目来说有点儿过头了”之类的炒作文章,想要看透它们并理解 GraphQL 对你的特定项目究竟意味着什么,可能并非易事。我希望通过这篇文章为你省去一些麻烦,并将过去几年我关于 GraphQL 的所有学习心得都集中到一篇博文中,帮助你做出更明智的决定。

你能相信我的意见吗?也许吧,毕竟你自己要有自己的判断!以下是我在 GraphQL 生态系统中做过的一些工作,仅供参考:

  • 在创建Slicknode(一个用于快速创建 GraphQL API 的框架和无头 CMS)的过程中,我已经构建了数千个 GraphQL API
  • 我编写了graphql-query-complexity,这是最流行的开源库,用于保护用 NodeJS 编写的 GraphQL 服务器免受 DoS 攻击。它也被一些大型开源框架(例如 TypeGraphQL 和 NestJS)使用。
  • 我已经将 12 年前的代码库迁移到 GraphQL
  • 我已经将一个库从 JavaScript 移植到 PHP,用于创建与 Relay 兼容的 GraphQL API
  • 我已经从一项服务中删除了 GraphQL,因为它最终被证明不太合适

所以请记住,我是 GraphQL 的忠实粉丝,但我也会尽力谈论其权衡。

GraphQL 只是一种炒作吗?

就目前而言,可以肯定地说 GraphQL 将会继续存在。自 2015 年开源以来,它获得了令人难以置信的关注。它已被众多财富 500 强企业采用,而且这些应用不会在一夜之间消失。

GraphQL 也激发了投资者的兴趣:围绕 GraphQL 构建产品的企业已经筹集了数亿美元的风险投资。

目前,所有主流编程语言都提供了 GraphQL 客户端和服务器,仅 JavaScript 版 GraphQL 库的每周下载量就已达到 550 万次。GraphQL 相关的工具使用起来非常愉快,它为你提供了丰富的选项,让你能够发挥创造力,解决实际问题。

什么是 GraphQL?

官网将 GraphQL 描述为“一种用于 API 的查询语言”,其流程解释为“描述你的数据 ➜ 请求你想要的 ➜ 获得可预测的结果”。

我喜欢把它想象成API 的 SQL。使用 SQL 时,您可以以声明的方式编写 SQL 查询,描述您想要加载或更改的数据,然后让 SQL 服务器找出执行实际操作的最佳方法。您不必关心后台发生什么,例如从磁盘、缓存或网络读取了哪些块,这些都对您隐藏了起来。当您的 SQL 服务器有新版本可用时,您可以安全地更新数据库并从所有性能改进中获益,等等,而无需更新代码库中的单个 SQL 查询。如果添加了新功能,您可以在应用程序的新部分使用这些功能,但不必更新应用程序的现有部分,因为旧查询仍将以相同的方式工作。

这与 GraphQL 的工作方式非常相似,只是针对 API:您以声明式的方式编写 GraphQL 查询,将其发送到服务器,服务器会找出加载数据的最佳方式。然后,它会以您在查询中指定的确切格式返回数据。现在,如果您需要不同的数据,只需更改查询,而无需服务器!这赋予 API 使用者前所未有的权力。您可以向 GraphQL 服务器发送任何有效的查询,它将按需返回正确的响应。这就像拥有无限的 REST API 端点,而无需在后端更改任何代码。

一个很好的类比:如果 REST 就像在餐厅点菜,那么 GraphQL 就是无限量自助餐。你可以将自助餐上的任何菜肴混合搭配在一个盘子里,想吃多少就吃多少。使用 REST,菜肴的份量总是相同的,你必须先问服务员是否可以组合多道菜,他们必须再去厨房确认,而且可能会回来告诉你他们不这样做,认为你是个讨厌的顾客,因为你不尊重他们的菜单。

此外,当您想要为 GraphQL 服务器添加新功能时,只需在架构中添加更多字段和类型即可。客户端应用程序中所有现有的 GraphQL 查询均可继续使用,无需任何更改。这使您能够独立地改进 API 和客户端应用程序,但我们稍后会更详细地讨论这一点。

如果您还不太熟悉 GraphQL,我建议您查看GraphQL 官方网站上的介绍,这样您就可以看到一些 GraphQL 查询和模式定义的实际操作。

为什么选择 GraphQL?

那么,最初为什么要创建 GraphQL,它应该解决什么问题?

GraphQL 最初是在 Facebook 创建的,用于解决服务器与其大量客户端应用程序之间的 API 通信的一些主要挑战,这些客户端应用程序适用于各种设备大小、操作系统、Web 应用程序等。你可以想象,在这种规模上开发 API 是极其困难的,特别是如果你使用 REST 风格的架构。

一部关于 GraphQL 的纪录片,其中 GraphQL 的创建者和其他早期采用者谈论了他们创建和采用 GraphQL 的原因。

让我们更详细地了解一下最重要的原因。

过度获取

REST API 的一个问题是,它有一个预定义的响应格式,非常不灵活。一个 URL 总是会返回请求的完整资源。当然,有一些方法可以缓解这个问题,比如通过参数传递要包含在响应中的字段等等,但这些方法尚未标准化,因此需要文档说明,并且必须在后端实现,这增加了不必要的复杂性。

随着时间的推移,这可能会成为一个问题,尤其是当您改进 API 并添加新功能或弃用过时的数据时。

例如:假设您想向用户对象添加一个新字段,并将当前在线状态添加到用户的 REST 端点:

GET https://example.com/users/2
{
  "username": "ivo"
}
Enter fullscreen mode Exit fullscreen mode

您可以简单地将该字段添加到响应中,然后响应将变成:

{
  "username": "ivo",
  "isOnline": true
}
Enter fullscreen mode Exit fullscreen mode

这很棒,而且有效。然而,这种方法的问题在于,即使客户端应用程序并不需要,在线状态也会被发送到每个客户端应用程序。多次重复这样的操作,最终会导致向每个客户端发送臃肿且几乎无用的响应,占用它们的带宽,并随着时间的推移使应用程序变慢。

未充分获取

构建富用户界面时,您可能熟悉的另一个挑战是所谓的“数据获取不足”:您无法在单个 API 请求中获取显示所需的所有数据,而必须再次调用 API 来加载相关数据。这会增加额外的服务器往返次数,增加延迟,并可能导致糟糕的用户体验。

让我们看一个非常简单的例子:假设您想要为博客文章详细信息页面创建后端,并在其中显示以下数据:

{
  "post": {
    "title": "GraphQL is awesome!",
    "text": "GraphQL solves so many problems...",
    "author": {
      "name": "Ivo"
    },
    "comments": [
      {
        "text": "100% !!!",
        "author": {
          "name": "John"
        }
      },
      {
        "text": "Couldn't agree more",
        "author": {
          "name": "Jane"
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

当您想使用 REST 实现这一点时,您必须问几个问题:
您是否将作者包含在帖子的回复数据中?

  • 为什么不把它放在专用的/user/23API 资源中以避免冗余并仅在帖子响应中返回引用?
  • 那评论呢?你们也会回复吗?
  • 评论的作者怎么样?
  • 它的尽头在哪里?

为了保持DRY,您可以像这样实现响应:

{
  "post": {
    "title": "GraphQL is awesome!",
    "text": "GraphQL solves so many problems...",
    "author": "/user/1",
    "comments": "/comments?post=345"
  }
}
Enter fullscreen mode Exit fullscreen mode

当客户端收到此响应时,它并不包含我们需要的所有数据。我们必须发出额外的请求来获取作者和评论,这会增加额外的延迟。我们可以创建一个自定义 API 端点,以我们所需的格式返回数据。但这可能会增加后端的冗余,并降低 API 的灵活性(移动应用程序可能不想在一开始就加载评论)。

GraphQL 通过让前端开发人员能够按需请求他们需要的数据并让 GraphQL 服务器自动完成加载(或不加载)引用的繁重工作来消除这个问题。

功能弃用

随着项目的发展和需求的变化,您可能希望弃用某个功能并将其从 API 中移除,以避免维护过时或冗余的服务。这对于 REST 架构来说可能是一个重大挑战,尤其是在更复杂的项目中。您是否会为每个移除的功能发布一个新版本?如何确保已移除的功能不会被某些客户端应用程序继续使用?何时可以关闭旧版本的 API?

很多情况下,保留旧功能比实施可靠的迁移策略耗费大量工程精力更简单、更经济。问题在于,你得强行让所有这些无用的数据通过用户带宽有限的连接传输,而且没有简单的解决方案,否则你就不得不维护多个版本的 API。

GraphQL 内置了解决这个问题的方法。您只需将某个字段标记为“已弃用”,并添加有关如何迁移客户端应用程序的信息即可。这些信息以标准化的方式提供给所有客户端应用程序。您可以在 CI 管道中运行一个脚本,自动检查是否使用了已弃用的字段,并相应地迁移客户端应用程序。一旦客户端应用程序中的所有弃用通知都得到修复,您就可以安全地从 GraphQL API 中移除该字段。

其好处显而易见:

  • 无需创建、运行和维护多个版本的 API。只需一个 GraphQL API,即可随项目不断发展。
  • 一种管理功能弃用的自动化和自文档化的方法。

解耦前端和后端开发

通过在项目中引入 GraphQL,您可以完全解耦前后端开发,从而消除大量的摩擦。您可以完全独立于后端开发和更改任意数量的前端应用程序。无需为特定视图创建或更新特定的 REST 端点,权力转移到前端开发人员身上,因为他们只需按需请求数据即可。

回到餐厅的类比:厨师只需在自助餐台上添加一道新菜,用户就可以在取餐时将其与自己点的其他菜品混合搭配。无需任何协调。与 REST 风格的点菜方式相比,更改菜单以组合多道菜需要与厨师协调,甚至可能需要与餐厅其他工作人员协调。

作为这在实践中意味着什么的一个例子:在我正在进行的一个项目中,一个团队创建了一个完整的移动应用程序,而没有改变 GraphQL 后端中的任何内容。

GraphQL 杀手级功能

GraphQL 有一个特性很少受到关注,甚至在探讨其优缺点的文章中也常常被忽略。在我看来,这正是GraphQL 的杀手级特性,它的价值无可估量,尤其是在大型项目中。我们目前讨论的所有 GraphQL 优势都可以以某种方式规避,虽然坦白说,需要做出一些妥协,但都不是万能的。然而,我还没发现有任何被广泛采用的技术能够像 GraphQL 那样解决这个问题:

数据依赖关系的共置

让我们看一个例子来说明这个问题。我们在代码库中的某个地方有一个 React 组件,用于显示用户名:

export function UserName({user}) {
  return (
    <span>{user.username}</span>
  );
}
Enter fullscreen mode Exit fullscreen mode

现在,我们想在用户名旁边显示在线状态。这听起来很简单的任务,很快就会变成一场噩梦。它引发了各种各样的问题:

  • 在线状态在用户对象中可用吗?
  • 你是怎么知道或找到答案的?有相关资料吗?
  • 如果数据来自 API,您如何确保它包含在返回组件用户对象的每个 API 端点中?

您需要掌握大量知识,而这些知识在您想要实现功能的地方并非唾手可得,而且也不一定与当前任务相关。对于刚接触代码库的开发者来说,这可能尤其棘手。您可能需要识别和更改大量 API 端点,并在所有包含用户对象的 API 响应中包含在线状态。

对于 GraphQL API 来说,这完全不是问题,因为您可以使用 GraphQL 片段将数据依赖关系与前端组件共置:

export const UserNameFragments = {
  user: gql`
    fragment UserName on User {
      username
      #Just add the online status to the fragment here:
      isOnline
    }
  `
}

export function UserName({user}) {
  return (
    <span>{user.username} ({user.isOnline ? "online" : "offline"})</span>
  );
}
Enter fullscreen mode Exit fullscreen mode

您可以使用 GraphQL 片段直接在 UI 组件中定义数据依赖关系。然后,父组件可以在加载数据的 GraphQL 查询中包含这些片段,这样就可以保证UserName组件无论位于应用程序的哪个位置,都能收到在线状态。

这使得扩展您的应用程序变得异常简单,无论它多么复杂。您无需了解代码库的其余部分,就可以自信地在不离开组件的情况下实现功能。借助合适的工具,您甚至可以在 IDE 中获得自动完成功能、类型验证和文档。

性能与安全

强大的力量带来强大的攻击面。

通过将 GraphQL API 暴露到互联网上,您将赋予客户端强大的功能,这可能会对安全性和性能产生重大影响。客户端可以按需一次性访问您的所有数据和功能。这大大增加了您的攻击面,如果一开始就不加以考虑,很容易被利用。

让我们看一些有问题的查询...

加载大量数据:

query LotsOfPosts {
  posts(first: 100000000) {
    title
  }
}
Enter fullscreen mode Exit fullscreen mode

加载需要数百万个数据库查询的深度嵌套数据:

query DeeplyNestedData {
  user(id: 2) {
    name
    friends {
      name
      friends {
        name
        friends {
          name
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

通过单个请求发起暴力攻击:

mutation BruteForcePassword {
  attempt1: login(email: "victim@example.com", password: "a")
  attempt2: login(email: "victim@example.com", password: "b")
  # ...
  attempt100000: login(email: "victim@example.com", password: "xxxxx")
}
Enter fullscreen mode Exit fullscreen mode

问题在于,这些查询不会被常见的速率限制器阻止。你向 GraphQL 服务器发送一个请求,就足以让服务器完全不堪重负。为了防止此类查询访问 GraphQL API,我编写了graphql-query-complexity,这是一个可扩展的开源库,它可以检测此类查询,并在服务器资源消耗过多之前拒绝不合理的查询。你可以为每个字段分配一个复杂度值,超过阈值的查询将被拒绝。在Slicknode中,此保护会根据返回的节点数量自动添加。

另一种常见方法是注册一个允许查询的白名单,并拒绝所有其他对 API 的查询。这可能比动态规则更细粒度、更安全,但它限制了 GraphQL API 的灵活性,并且您必须使查询注册表与所有客户端应用程序保持同步更新,这需要额外的设置和维护。

优化对内部数据存储的请求可能是另一个挑战。优化数据加载过程的责任完全由 GraphQL API 承担,无法轻易地转移到 CDN 或反向代理。使用 REST API,由于 REST 端点的作用域有限,您可以很好地控制执行多少个数据库查询以及执行哪些查询。使用 GraphQL,客户端可以请求任意数量的对象,这些对象可能需要从不同的数据库表中加载。Slicknode 会通过分析GraphQL查询并动态生成 SQL 查询,自动将多个请求的对象组合成一个数据库查询,但普通的 ORM 可能不具备开箱即用的功能。

这也与 N+1 问题相关,嵌套查询会导致数据库请求数量激增。如果您想了解更多关于此问题的信息,我推荐您观看此视频,并了解一下dataloader,这是 Facebook 发布的一个库,用于帮助批量处理查询并解决此问题。

缓存与 GraphQL

缓存始终是一项艰巨的挑战,对于 GraphQL 服务器来说尤其如此。我们通常依赖的很多工具都无法与 GraphQL 很好地兼容。

以 CDN 为例。部署 GraphQL API 最常见的方式是通过 HTTP 服务器。然后,您可以通过 POST 请求将 GraphQL 请求发送到 API 并获取响应。问题在于,POST 请求在最常见的 CDN 中默认不会被缓存。您也可以通过 GET 请求发送请求,但由于 GraphQL 查询可能会非常庞大​​,因此很快就会达到请求大小限制。如果您使用了查询允许列表,则可以向服务器发送查询 ID 或哈希值,而不是完整的查询,以绕过此限制。

使用 GraphQL API 时,缓存失效也可能更具挑战性。所有查询通常都通过同一个 URL 进行处理,因此无法通过 URL 来使资源失效。此外,一个数据对象可以包含在任意数量的缓存响应中。解决这个问题的一种策略是将缓存标签附加到响应中,然后根据这些标签而不是 URL 来使响应失效。Slicknode Cloud 正是采用这种方法在全球范围内缓存 GraphQL 响应。

为 GraphQL API 添加缓存的一个好方法是在 GraphQL API 本身后面添加一个层,并将其实现在数据源之前。将其与“性能与安全”中提到的数据加载器结合使用,您可以完全自定义缓存行为。

单点故障

需要记住的是,GraphQL API 将成为 API 网关的替代品,用于访问所有功能、数据和服务。如果 GraphQL API 出现故障,整个应用程序都会离线。这与 REST 架构并无太大区别,但需要注意的是,GraphQL API 将成为基础设施的重要组成部分,并应妥善处理。

GraphQL 的最佳(和最差)用例

如果你手里有把锤子,所有问题看起来都像钉子。GraphQL 提供的所有优势真的很容易让人爱上它。与之前的技术相比,它让前端开发者的工作变得轻松很多。但有些类型的应用程序比其他类型的应用程序更适合 GraphQL。我也曾有过类似的经历,从应用程序的某些部分移除了 GraphQL,而对于其他应用程序,我强烈推荐它。

根据我的经验,GraphQL 的最佳用例正是其最初的设计初衷:为丰富的用户界面提供数据和功能。一个包含所有数据和功能的统一 GraphQL API,可供任意数量的团队轻松访问,始终保持更新,并具备自文档功能。它显著降低了前端代码的复杂性。以前,您必须实现大量 API 调用,并承担异步功能所带来的所有复杂性(加载状态、错误处理等),现在您只需定义数据依赖关系,剩下的交给 GraphQL 处理即可。您可以在构建时验证所有 API 调用,并实现端到端类型安全的解决方案。尽管 GraphQL 对于个人开发者来说已经非常出色,但随着应用程序和团队规模的扩大,您使用 GraphQL 的乐趣也会随之增加。

那么,什么时候应该考虑 GraphQL 的替代方案呢?我会考虑那些希望在物理上隔离不同服务的应用程序。GraphQL 非常适合将大量功能整合到一处。但如果您想要在网络或硬件层面隔离某些服务,并且只允许一部分服务访问它们,这可能会成为一个问题。您最好考虑其他架构。

结论

GraphQL 是开发者工具箱中一个非常棒的补充,尤其是在支持用户界面方面。使用它是一件令人愉悦的事情,我很高兴看到 GraphQL 生态系统越来越受到关注。为了让开发者更轻松地构建 GraphQL API,我创建了Slicknode 。它可以自动化所有复杂部分,让您在几分钟内即可启动并运行。快来加入精彩的 GraphQL 社区吧!我很快还会分享一些关于Slicknode 的重磅消息,所以请务必在 Twitter 上关注我并订阅新闻通讯,这样您就能第一时间了解。

鏂囩珷鏉ユ簮锛�https://dev.to/ivomeissner/to-graphql-or-not-to-graphql-pros-and-cons-11bl
PREV
类型系统如何改进你的 JavaScript 代码
NEXT
片段:getContext 与 requireContext