2019年1月31日

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

过去两年间,GraphQL 服务器开发工具呈爆炸式增长。我们认为大多数工具的需求源于流行的“schema-first”方法——而另一种替代方案“code-first”可以解决这些问题。

The Problems of "Schema-First" GraphQL Server Development
 
(当前正在阅读)
“Schema-First” GraphQL 服务器开发的问题
 
介绍 GraphQL Nexus:Code-First GraphQL 服务器开发
 
将 GraphQL Nexus 与数据库结合使用

概述:从 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 消费者需求的风险。

在本文中,我们将讨论一种开发过程的缺点:GraphQL 模式首先在 SDL 中*手动*定义,然后才实现解析器。在这种方法中,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:Schema-first 因 graphql-tools 而普及

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

目标是将模式*定义*与实际*实现*分离,这催生了当前流行的*模式驱动*或 *schema-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 模式

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

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

结论: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-rubysangria-graphql,以及适用于 Python 的 graphene 或适用于 Elixir 的 absinthe-graphql



Code-first 的实践

虽然本文主要旨在理解 SDL-first 的问题,但这里简要介绍一下使用 code-first 框架构建 GraphQL 模式是什么样子


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

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

一旦所有 GraphQL 类型定义完毕,它们就会被传递到一个函数中,以创建一个可在 GraphQL 服务器中使用的 GraphQLSchema 实例。通过指定 outputs,您可以定义生成的 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 电子报

© . All rights reserved.