在过去两年里,GraphQL 服务器开发的工具生态系统呈现爆发式增长。我们认为,大多数工具的需求源于流行的 schema-first 方法——而这个问题可以通过另一种方法解决:code-first。

概述:从 schema-first 到 code-first
本文概述了当前 GraphQL 服务器开发领域的现状。以下是本文涵盖内容的快速概要:
- 本文中“schema-first”是什么意思?
- GraphQL 服务器开发的演变
- 分析 SDL-first 开发的问题
- 结论:SDL-first 可能可行,但需要大量工具
- 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 服务器开发的演变

阶段 1:使用 graphql-js
的早期
GraphQL 于 2015 年发布时,工具生态系统还很匮乏。只有官方规范及其 JavaScript 参考实现:graphql-js
。直到今天,graphql-js
仍用于最流行的 GraphQL 服务器中,例如 apollo-server
、express-graphql
和 graphql-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-driven 或 schema-first / SDL-first 开发流程
- 在 GraphQL SDL 中手动编写 GraphQL schema 定义
- 实现所需的 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 的同步。例如,通过使用 graphqlgen
或 graphql-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 的 GraphQL 或 Apollo GraphQL 插件。
问题 5:组合 GraphQL schema
将 schema 模块化的想法也引出了另一个问题:如何将多个现有(且分布式的)schema 组合成一个单一的 schema。
工具/解决方案:最流行的 schema 组合方法是 schema stitching,它也是前面提到的 graphql-tools
库的一部分。为了更精确地控制 schema 如何组合,您还可以直接使用 schema delegation(它是 schema stitching 的一个子集)。
结论:SDL-first 可能可行,但需要大量工具
在探讨了问题领域以及为解决这些问题而开发的各种工具之后,似乎 SDL-first 开发可能最终可行——但同时也需要开发人员学习和使用大量的额外工具。
权宜之计,权宜之计,权宜之计,...
在 Prisma,我们在推动 GraphQL 生态系统发展方面发挥了重要作用。许多提到的工具都是由我们的工程师和社区成员构建的。
经过几个月的开发以及与 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-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 新闻通讯