2019 年 1 月 31 日

“Schema-First” GraphQL 服务端开发的问题

在过去两年中,GraphQL 服务端开发工具呈爆炸式增长。我们认为,大多数工具的需求都来自于流行的 schema-first 方法 —— 并且可以通过另一种方法解决:code-first。

The Problems of "Schema-First" GraphQL Server Development

概述:从 Schema-First 到 Code-First

本文概述了当前 GraphQL 服务端开发的现状。以下是涵盖内容的快速概要

  1. 在本文中,“schema-first” 是什么意思?
  2. GraphQL 服务端开发的演变
  3. 分析 SDL-first 开发的问题
  4. 结论:SDL-first 可能可以工作,但需要大量的工具
  5. Code-first:GraphQL 服务端开发的一种符合语言习惯的方式

虽然本文主要提供 JavaScript 生态系统的示例,但其中大部分也适用于其他语言生态系统中的 GraphQL 服务端开发。


在本文中,“schema-first” 是什么意思?

术语 schema-first 非常模糊,通常传达一个非常积极的想法:使模式设计成为开发过程中的优先事项。

在实现 API 之前先考虑模式(以及因此的 API)通常会产生更好的 API 设计。如果模式设计不足,则存在最终得到的 API 是后端如何实现的产物,而忽略了业务领域的原语和 API 使用者的需求的风险。

在本文中,我们将讨论首先在 SDL 中手动定义 GraphQL 模式,然后在之后实现解析器的开发过程的缺点。在这种方法中,SDL 是 API 的真理来源为了澄清 schema-first 设计与这种特定实现方法之间的区别,从现在开始我们将称之为 SDL-first

相比之下,code-first(有时也称为 resolver-first)是一个过程,其中 GraphQL 模式是以编程方式实现的,并且模式的 SDL 版本是其生成的工件。使用 code-first,您仍然可以非常关注前期的模式设计!


GraphQL 服务端开发的演变

The evolution of GraphQL server development

阶段 1:使用 graphql-js 的早期

当 GraphQL 在 2015 年发布时,工具生态系统还很匮乏。只有官方的 规范 及其在 JavaScript 中的参考实现:graphql-js。直到今天,graphql-js 仍被用于最流行的 GraphQL 服务器中,例如 apollo-serverexpress-graphqlgraphql-yoga

当使用 graphql-js 构建 GraphQL 服务器时,GraphQL 模式 被定义为纯 JavaScript 对象

从这些示例可以看出,使用 graphql-js 创建 GraphQL 模式的 API 非常冗长。模式的 SDL 表示形式更加简洁易懂

这篇文章中了解更多关于使用 graphql-js 构建 GraphQL 模式的信息。

阶段 2:graphql-tools 普及了 Schema-first

为了简化开发并提高实际 API 定义的可见性,Apollo 开始在 2016 年 3 月构建 graphql-tools 库(这里是第一次提交)。

目标是将模式定义与实际实现分离,这导致了目前流行的schema-drivenschema-first / SDL-first 开发过程

  1. 在 GraphQL SDL 中手动编写 GraphQL 模式定义
  2. 实现所需的解析器函数

使用这种方法,上面的示例现在看起来像这样

这些代码片段与上面使用 graphql-js 的代码 100% 等效,只是它们更具可读性,更容易理解。

可读性不是 SDL-first 的唯一优势

  • 该方法易于理解,并且非常适合快速构建事物
  • 由于每个新的 API 操作都首先需要在模式定义中体现,GraphQL 模式设计不是事后才考虑的
  • 模式定义可以作为 API 文档
  • 模式定义可以作为 前端和后端团队之间的沟通工具 —— 前端开发人员被赋能,更多地参与到 API 设计中
  • 模式定义支持 API 的快速模拟

阶段 3:开发新工具来“修复” SDL-first

虽然 SDL-first 有许多优点,但过去两年表明,将其扩展到更大的项目具有挑战性。在更复杂的环境中出现了一些问题(我们将在下一节中详细讨论这些问题)。

这些问题本身确实大多是可以解决的 —— 实际问题是,解决这些问题需要使用(和学习)许多额外的工具。在过去两年中,已经发布了大量的工具,试图改进围绕 SDL-first 开发的工作流程:从编辑器插件到 CLI 再到语言库。

学习、管理和集成所有这些工具的开销减慢了开发人员的速度,并使他们难以跟上 GraphQL 生态系统的步伐。


分析 SDL-first 开发的问题

现在让我们更深入地探讨围绕 SDL-first 开发的问题领域。请注意,这些问题中的大多数尤其适用于当前的 JavaScript 生态系统。

问题 1:模式定义和解析器之间的不一致

使用 SDL-first,模式定义必须与解析器实现的精确结构匹配。这意味着开发人员需要确保模式定义始终与解析器同步!

虽然即使对于小模式来说这已经是一个挑战,但随着模式增长到数百甚至数千行(作为参考,GitHub GraphQL 模式 超过 1 万行),这实际上变得不可能。

工具/解决方案: 有一些工具可以帮助保持模式定义和解析器同步。例如,通过使用 graphqlgengraphql-code-generator 等库进行代码生成。

问题 2:GraphQL 模式的模块化

在编写大型 GraphQL 模式时,您通常不希望所有的 GraphQL 类型定义都驻留在同一个文件中。相反,您希望将它们拆分成更小的部分(例如,根据功能产品)。



工具/解决方案: 诸如 graphql-import 或更新的 graphql-modules 库之类的工具可以帮助解决这个问题。graphql-import 使用自定义导入语法编写为 SDL 注释。graphql-modules 是一套工具,旨在帮助模式分离解析器组合以及为 GraphQL 服务器实现可扩展结构

问题 3:模式定义中的冗余(代码重用)

另一个问题是如何重用 SDL 定义。这个问题的常见示例是 Relay 样式的连接。虽然它们提供了一种实现分页的强大方法,但它们需要大量的样板代码和重复代码。

目前没有工具可以帮助解决这个问题。开发人员可以编写自定义工具来减少重复代码的需求,但目前该问题缺乏通用解决方案。

问题 4:IDE 支持 & 开发者体验

GraphQL 模式基于强大的类型系统,这在开发过程中可能是一个巨大的优势,因为它允许对您的代码进行静态分析。不幸的是,SDL 通常在您的程序中表示为纯字符串,这意味着工具无法识别其中的任何结构。

那么问题就变成了如何在您的编辑器工作流程中利用 GraphQL 类型,从而受益于 SDL 代码的自动完成和构建时错误检查等功能。

工具/解决方案: graphql-tag 库公开了 gql 函数,该函数将 GraphQL 字符串转换为 AST,从而实现静态分析以及由此产生的功能。除此之外,还有各种编辑器插件,例如 VS Code 的 GraphQLApollo GraphQL 插件。

问题 5:组合 GraphQL 模式

模块化模式的想法也引出了另一个问题:如何将多个现有的(和分布式的)模式组合成一个单一的模式。

工具/解决方案: 最流行的模式组合方法一直是 模式拼接,它也是前面提到的 graphql-tools 库的一部分。为了更好地控制模式的组合方式,您还可以直接使用 模式委托(它是模式拼接的子集)。

结论:SDL-first 可能可以工作,但需要大量的工具

在探索了问题领域和为解决这些问题而开发的各种工具之后,似乎 SDL-first 开发可能最终可以工作 —— 但也需要开发人员学习和使用大量的额外工具。



变通方法,变通方法,变通方法,...

在 Prisma,我们在推动 GraphQL 生态系统向前发展方面发挥了重要作用。许多提到的工具都是由我们的工程师和社区成员构建的。

Workarounds cartoon

经过几个月的开发以及与 GraphQL 社区的密切互动,我们已经意识到我们只是在解决症状。这就像与 九头蛇 作战 —— 解决一个问题会导致几个新问题。

生态系统锁定:购买整个工具链

我们非常感谢我们在 Apollo 的朋友们的工作,他们不断努力改进围绕 SDL-first 开发的开发工作流程。

以 SDL-first 方式构建 GraphQL 服务器的另一个流行示例是 AWS AppSync。它与 Apollo 模型略有不同,因为解析器(通常)不是以编程方式实现的,而是从模式定义自动生成的。

虽然社区从如此多的工具中受益匪浅,但当开发人员需要完全押注某个组织的工具链时,存在生态系统锁定的风险。真正的解决方案可能是将许多 SDL-first 意见融入到 GraphQL 核心本身中 —— 这在可预见的未来不太可能发生。

SDL-first 忽略了编程语言的个体特征

SDL-first 的另一个问题方面是,它忽略了编程语言的个体特征,无论使用哪种编程语言,都施加类似的原则。

Code-first 方法在其他语言中运行良好:Scala 库 sangria-graphql 利用 Scala 强大的类型系统来优雅地构建 GraphQL 模式,graphlq-ruby 使用了 Ruby 语言的许多令人敬畏的 DSL 功能。


Code-first:GraphQL 服务端开发的一种符合语言习惯的方式

您唯一需要的工具是您的编程语言

大多数 SDL-first 问题都来自于我们需要将手动编写的 SDL 模式映射到编程语言的事实。这种映射是导致需要额外工具的原因。如果我们遵循 SDL-first 路径,则需要在每种语言生态系统中重新发明所需的工具,并且每种工具的外观也会有所不同。

与其使用更多工具来增加 GraphQL 服务端开发的复杂性,我们应该努力寻求更简单的开发模型。理想情况下,一种让开发人员利用他们已经使用的编程语言的模型 —— 这就是 code-first 的理念。

code-first 到底是什么?

还记得在 graphql-js 中定义模式的最初示例吗?这就是 code-first 含义的本质。没有手动维护的模式定义版本,相反,SDL 是从实现模式的代码生成的。

虽然 graphql-js 的 API 非常冗长,但在其他语言中有很多流行的框架基于 code-first 方法工作,例如前面提到的 graphlq-ruby sangria-graphql,以及 Python 的 graphene 或 Elixir 的 absinthe-graphql



Code-first 实践

虽然本文主要关于理解 SDL-first 的问题,但这里有一个关于使用 code-first 框架构建 GraphQL 模式是什么样子的简短预告


使用这种方法,您可以直接在 TypeScript/JavaScript 中定义您的 GraphQL 类型。通过正确的设置,并感谢 智能代码完成,您的编辑器将能够在您定义它们时建议可用的 GraphQL 类型、字段和参数。

典型的编辑器工作流程包括在后台运行的开发服务器,该服务器在每次保存文件时都会重新生成类型定义。

一旦定义了所有 GraphQL 类型,它们将被传递到一个函数中,以创建一个 GraphQLSchema 实例,该实例可以在您的 GraphQL 服务器中使用。通过指定 ouputs,您可以定义生成的 SDL 和类型定义应位于何处。

本系列文章的后续部分将更详细地讨论 code-first 开发。

获得 SDL-first 的好处,而无需所有工具

前面我们列举了 SDL-first 开发的好处。事实上,在使用 code-first 方法时,无需在大多数好处上妥协。

使用 GraphQL 模式作为前端和后端团队之间关键沟通工具的最重要好处仍然存在。

以 GitHub GraphQL API 为例:GitHub 使用 Ruby 和 code-first 方法来实现他们的 API。SDL 模式定义是基于实现 API 的代码生成的。但是,模式定义仍然被检入版本控制。这使得在开发过程中跟踪 API 的更改变得非常容易,并改善了各个团队之间的沟通。

诸如 API 文档或赋能前端开发人员等其他好处也不会因 code-first 方法而丢失。

Code-first 框架,即将来到您的 IDE

本文相当理论化,没有包含太多代码 —— 我们仍然希望我们能够激发您对 code-first 开发的兴趣。要查看更多实际示例并了解更多关于 code-first 开发体验的信息,请保持关注并在接下来的几天内密切关注 Prisma Twitter 帐户 👀

您如何看待这篇文章?加入 Prisma Slack 与其他 GraphQL 爱好者讨论 SDL-first 和 code-first 开发。


🙏 非常感谢 SashkoApollo 团队对本文的反馈!

不要错过下一篇文章!

注册 Prisma Newsletter