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 有些模棱两可,但通常传达了一个非常积极的想法:在开发过程中优先考虑 schema 设计。

在实现 schema(以及 API)之前考虑它通常会带来更好的 API 设计。如果 schema 设计不足,则存在最终得到一个 API 的风险,该 API 是后端如何实现的产物,忽略了业务领域的原语和 API 消费者的需求。

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

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


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 schema 被定义为一个普通的 JavaScript 对象

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

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

阶段 2:Schema-first 由 graphql-tools 推广

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

目标是将 schema 定义与实际实现分开,这导致了目前流行的 schema 驱动或 schema-first / SDL-first 开发流程

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

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

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

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

  • 这种方法易于理解,非常适合快速构建事物
  • 由于每个新的 API 操作首先需要在 schema 定义中体现出来,因此 GraphQL schema 设计不是事后才考虑的
  • schema 定义可以用作 API 文档
  • schema 定义可以用作前端和后端团队之间的沟通工具——前端开发人员正在获得授权并更多地参与 API 设计
  • schema 定义支持快速模拟 API

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

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

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

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


分析 SDL-first 开发的问题

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

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

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

虽然即使对于小型 schema 来说这已经是一个挑战,但随着 schema 增长到数百或数千行(作为参考,GitHub GraphQL schema 有超过 10k 行),这实际上变得不可能。

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

问题 2:GraphQL schema 的模块化

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



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

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

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

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

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

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

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

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

问题 5:组合 GraphQL schema

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

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

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

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



变通方案,变通方案,变通方案,...

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

Workarounds cartoon

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

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

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

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

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

SDL-first 忽略了编程语言的个别特性

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

Code-first 方法在其他语言中效果非常好:Scala 库 sangria-graphql 利用 Scala 强大的类型系统来优雅地构建 GraphQL schema,graphlq-ruby 使用了 Ruby 语言的许多出色的 DSL 功能。


Code-first:GraphQL 服务器开发的语言惯用方式

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

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

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

code-first 到底是什么?

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

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



Code-first 的实践

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


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

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

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

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

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

早些时候,我们列举了 SDL-first 开发的好处。事实上,当使用 code-first 方法时,没有必要在大多数好处上妥协。

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

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

诸如 API 文档或授权前端开发人员等其他好处也不会随着 code-first 方法的采用而丢失。

Code-first 框架,即将进入您的 IDE

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

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


🙏 非常感谢 Sashko 和 Apollo 团队对本文的反馈!

不要错过下一篇文章!

注册 Prisma 新闻通讯