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 设计。

在实现 API 之前思考 schema(以及因此而思考 API),通常会带来更好的 API 设计。如果 schema 设计不足,则存在 API 设计最终只是后端实现方式的结果的风险,这会忽略业务领域的原语和 API 消费者的需求。

在本文中,我们将讨论一种开发流程的缺点,在这种流程中,GraphQL schema 首先在 SDL 中手动定义,然后实现 resolver。在这种方法中,SDL 是 API 的真相来源(source of truth)为了澄清 schema-first 设计与这种特定实现方法之间的区别,从现在起我们将称之为 SDL-first

相比之下,code-first(有时也称为 resolver-first)是一个通过编程方式实现 GraphQL schema 的过程,而 schema 的 SDL 版本是该过程生成的产物(artifact)。使用 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:由 graphql-tools 普及的 Schema-first

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

其目标是将 schema 的定义与实际的实现分离,这催生了当前流行的 schema-drivenschema-first / SDL-first 开发流程

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

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

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

可读性并非 SDL-first 的唯一优势

  • 这种方法易于理解,并且非常适合快速构建事物
  • 由于每个新的 API 操作都需要首先在 schema 定义中体现出来,GraphQL schema 设计不再是事后考虑
  • schema 定义可以作为 API 文档
  • schema 定义可以作为前后端团队之间的沟通工具——前端开发人员因此获得能力提升,并更积极地参与 API 设计
  • schema 定义使得快速模拟 API 成为可能

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

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

这些问题本身确实大多是可解决的——实际问题在于解决它们需要使用(并学习)许多额外的工具。在过去两年中,涌现了大量工具,试图改进 SDL-first 开发的工作流程:从编辑器插件、命令行工具到语言库。

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


分析 SDL-first 开发的问题

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

问题 1:schema 定义与 resolver 之间的不一致

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

即使对于小型 schema 来说,这已经是一个挑战,但当 schema 增长到数百或数千行时,这几乎变得不可能(作为参考,GitHub GraphQL schema 有超过 1 万行)。

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

问题 2:GraphQL schema 的模块化

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



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

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

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

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

问题 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 模型略有不同,因为 resolver 通常不是通过编程方式实现的,而是从 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-rubysangria-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 开发。


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

不要错过下一篇文章!

订阅 Prisma 新闻通讯