GraphQL API 设计指南

共 3527字,需浏览 8分钟

 ·

2022-04-25 12:28


使用 NestJs 开发 GraphQL API 的一些经验分享


GraphQLGraphQL 是一个开源的 API 数据查询操作语言,也是一个为实现查询已有数据的运行时。Facebook 内部在2012年开发了 GraphQL 并应用在了 Facebook App 上,在2015年公开发行。2018年11月7日,GraphQL 项目从 …


知乎专栏



上一篇以自己的万能 BFF 项目为例,聊了一下什么是 GraphQL 以及如何使用 NestJs 框架来开发 GraphQL API ,比较干。今天聊一聊湿一点的内容,GraphQL API 的设计。GraphQL API 的设计原则和 restful API 的设计原则还挺不一样,关于 Restful API 的设计,详见该专栏的讨论:


接口(API)开发应该遵循的原则


原则原则构成设计必须达到的基准,最佳实践提供基于之前的经验总结出来的指引和模式。1. 数据模型驱动基于数据和功能的接口应该基于良好定义的数据模型、概念和术语。接口对外暴露的应该是数据对象/它们代表的概…


知乎专栏



首先强调一下我个人认为的最重要的两条指导原则,然后在这两条最重要的原则的指导下,引申阐述设计细节。向后兼容老版本这是我认为最重要的一条,但是在实际的团队开发中,却总是有成员违反的一条。总是有人尝试带来破坏性的改动,而且在代码审核环节被提出有兼容问题的时候,总是以前后端一起发版,没有问题来反驳。实际上,完全的同时发版,不仅做不到,更加不是一个好的实践。好的实践是不同的服务和不同的微前端,各自可以随时独立发版,不要有依赖顺序。如果做到这一点很难,那么,允许前端在服务发版之后再行发版,是必须要做到的,不能再妥协了。而且,就算同时发布,有些前端程序要推送到所有设备是有一个时间延迟的。比如微信小程序前端的更新,要100%完成,是需要最长24小时的。也就是说,最长24小时的时间里,会同时存在新老两个版本。如果 GraphQL API 不能兼容老版本,就会有用户在这段时间里碰到问题。因此,原则是已有的查询无论如何不能被破坏。推论一:一旦一个字段被发布到了线上,就不能被删除了。如果真的需要删除它,那么,先将该字段标记为已废弃。当确保所有的前端都已经相应地更新完毕,再进行安全删除。在敏捷开发中,建议至少在前端修改上线后的下一个迭代周期里进行字段的删除修改。推论二:和推论一一样,一旦一个字段发布到了线上,就不能重命名了。实在需要重命名,那么请先新建一个字段,同时将旧字段标记为已废弃。确保所有前端对旧字段的引用改成使用新字段后,再对旧字段进行安全删除。向前考虑扩展性对所有的查询和操作,总是使用自定义对象类型(无论是输入参数还是响应负载都应如此)。否则,你就移除了未来向已有的查询和操作添加其他返回类型及元数据的可能性。而在设计无版本的 GraphQL API 中,提前压缩设计空间绝对不是你真正想做的。并且,原始类型迷恋也是一种代码坏味道(https://refactoring.guru/smells/primitive-obsession)。

AB

不使用自定义的对象类型,还会使得参数列表随着迭代变得越来越长,而一个长的参数列表,又是另一种代码坏味道。


在这两条基本原则的指导下,可以引申出很多设计细节:设计细节命名对于 Mutation,推荐动词+名词(操作目标对象)的驼峰命名方式,比如:createOrderupdateProfile……但是,Shopify 推荐 Mutation 使用名词+动词的形式,主要是出于将操作应用的目标对象在文件排序时能够放在一起。https://github.com/Shopify/graphql-design-tutorial精细化对于操作的种类不要害怕过于精细化了,相反,为每一种可能的 UI 更新都定义一种操作都不过分。和用户能够执行的动作一一对应的精细化的操作比泛化的操作更加强大。这是因为精细化的操作对 UI 开发者来说更容易写,并且可以由后端开发做进一步优化。而且,通过提供更精细化的 API 操作,会让恶意攻击者不正当使用这些 API 的尝试变得更加困难。举个例子,一个典型的 restful API 通常包含增删改查操作,但是在 GraphQL 里,推荐使用更加精细化的操作定义,就是说,不仅有增删改查,还有更多和 UI 操作相关的 API 定义,比如:createSomethingupdateSomethingdeleteSomethingpublishSomethingunpublishSomething……输入对象参数这是由向后兼容老版本和向前考虑扩展性的原则推论而来。因为未来的变化无法预测,所以需要留有后路。如果参数需要变化的话,那么定义一个自定义的对象,就可以将未来的参数变化控制在这个对象范围内(比如可以增加字段),而不用修改 API 的签名。并且可以兼容老版本,因为没有增加新的参数。唯一的返回对象类型同上,不仅输入参数应该是自定义类型,而且返回的数据体也应该是自定义的并且每个操作都唯一的类型。将受到操作影响的对象作为结果返回不要只返回 {success: true} 这样的结果,而是将受到操作影响的对象数据返回,这样使得更新前端的状态变得简单,而且可以保持更好的数据一致性。嵌套在 GraphQL 的世界里,套娃是一种美德。原因还是和前面讲过的一样,即可以向前兼容老版本和向后扩展性更好。单一 HTTP 端点虽然 GraphQL 是可以通过多个 HTTP 端点提供服务,但是非常建议仅仅通过单个 HTTP 端点来提供所有的 GraphQL 服务。这不像 Restful API 会为同一个资源的不同操作暴露不同的 URL,GraphQL 需要使用单一端点,否则在使用 GraphiQL、PlayGround 以及 Apollo Studio 时会碰到困难。


使用 Gzip 压缩 JSON 响应体鼓励生产环境的 GraphQL 服务开启 Gzip 压缩,而且客户端最好发送一个这样的 HTTP Header:Accept-Encoding: gzip


前端和 API 的开发人员都非常熟悉 JSON,它很易读和调试。

事实上,GraphQL 的语法就是受 JSON 的启发而来的,但是 JSON 响应不如二进制响应那样小巧,所以启用压缩在节约传输带宽上很有帮助。无版本GraphQL 仅仅返回客户端显式指定的数据,所以新的能力可以通过新的类型和新的字段来增加,而不会带来破坏性的改动。这就带来了一个通用的实践:总是通过无版本的 API 来避免破坏性改动。可空性在设计 GraphQL 的 Schema 时,要记住很重要的一点:事情总会有出错的可能,所以当获取一个字段的值可能碰到错误时,该字段就应该允许为空,否则在这种情况下会导致整个 API 报一个 Schema 相关的错误,从而引起整个前端界面崩掉的风险。除非肯定不会出错,这样的字段才能够设置为不可为空的类型。分页很多字段有可能返回长长的列表,这样的字段就应该接受分页参数,如 first、after 等,从而允许查询列表中的特定范围。after 是列表中的条目的唯一标识符。要设计强大的分页功能,有一个最佳实践。即:Connections。有一些 GraphQL 的客户端工具如 Relay,会知道如何与实现了这种 Connections 模式的 GraphQL 服务打交道,并自动提供一些客户端分页支持。


服务器端要支持批量操作并启用缓存详见该专栏的讨论:


Free Arch:给 GraphQL 加上服务器端响应缓存


题外话(声明本文和图数据库无关) 最近写了几篇关于 GraphQL 的文章:《 一顿操作猛如虎,部署一个万能 BFF》 《使用万能 BFF,将语雀文章 GraphQL 服务化》 《在小程序里接入 GraphQL》 《Free Arch:给 GraphQ…


知乎专栏


SchemaSchema 可以用来自动生成代码、验证、解析、开启内省模式,并给 API 带来类型安全。在 GraphQL 世界里,我们根据业务模型来定义 Schema;在 Schema 里,我们为 nodes 定义不同的类型,以及它们如何互相联结。即 GraphQL Schema 是应用的数据图以及操作的文本化表示。数据图定义了实体和它们之间的关系。要完成 Schema,需要添加 GraphQL 操作,这些操作提供了生成和操作数据图的信息。


浏览 56
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报