RESTful 架构基础
REST,代表表现层状态转移(Representational State Transfer),长久以来一直是 API 服务的圣杯,最初由 Roy Fielding 在其博士论文中定义。尽管它不是构建 API 的唯一方法,但由于其广泛的普及,即使是非开发者也对其有所了解。
RESTful 软件具有六个关键特征:
- 客户端-服务器架构
- 无状态性
- 可缓存性
- 分层系统
- 按需代码(可选)
- 统一接口
但这些还太理论化了,我们需要一些更具操作性的内容,那就是 API 成熟度模型。
Richardson 成熟度模型
由 Leonard Richardson 开发,该模型将 RESTful 开发的原则合并为四个易于遵循的步骤。
等级 0:POX 的沼泽
一个 0 级 API 是一组简单的 XML 或 JSON 描述。在介绍中,我提到在 Fielding 的论文之前,RESTful 原则被称为 “HTTP 对象模型”。
这是因为 HTTP 协议是 RESTful 开发的最重要部分。REST 围绕尽可能多地使用 HTTP 的固有属性的理念展开。
在 0 级,你根本不使用这些东西。你只是构建自己的协议并将其用作专有层。这种架构被称为远程过程调用(RPC),它非常适合远程过程/命令。
你通常有一个端点来接收一堆 XML 数据。例如 SOAP 协议:
另一个很好的例子是 Slack API。它稍微多样化一些,有几个端点,但它仍然是 RPC 风格的 API。它暴露了 Slack 的各种功能,中间没有增加任何功能。以下代码允许你向特定频道发布消息。
尽管它是根据 Richardson 的模型是 0 级 API,但这并不意味着它不好。只要它可用并能正确服务于业务需求,它就是一个很棒的 API。
等级 1:资源
要构建一个 1 级 API,你需要在系统中找到名词,并通过不同的 URL 暴露它们,如下例所示。
/api/books 将带我进入通用书籍目录。/api/profile 将带我进入这些书的作者的个人资料(如果只有一个的话)。要获取资源的第一个具体实例,我在 URL 中添加 ID(或其他引用)。
我还可以在 URLs 中嵌套资源,并显示它们是如何层次化组织的。
回到 Slack 的例子,这是它作为 1 级 API 的样子:
URL 发生了变化;现在我们有了/api/channels/general/messages 代替/api/chat.postMessage。
“channel”部分的信息已从正文移到URL中。这确实表明使用这个 API,你可以期待将消息发布到 general 频道。
等级 2:HTTP 动词
一个 2 级 API 利用 HTTP 动词添加更多的含义和意图。这些动词有很多,我只使用一小部分基本的:PUT / DELETE / GET / POST。
使用这些动词,我们期望含有它们的 URLs 展现不同的行为:
- POST—创建新数据
- PUT—更新现有数据
- DELETE—移除数据
- GET—寻找特定 id 的数据输出,或获取资源(或整个集合)
或者,使用之前的 /api/books 示例:
“安全”和“幂等”的含义是什么?
“安全”的方法是不会改变数据的方法。REST 建议 GET 只应该用来获取数据,因此它是上述集合中唯一的安全方法。不论你调用一个基于 REST 的 GET 方法多少次,它都不应该在数据库中改变任何东西。但这并不固有于动词——这取决于你如何实现它,所以你需要确保这一点。所有其他方法将以不同的方式改变数据,不能随机使用。在 REST 中,GET 既是安全的也是幂等的。
一个“幂等”的方法是在多次使用中不会产生不同结果的方法。根据 REST 的说法,DELETE 应该是幂等的——如果你一次删除一个资源,然后再次调用 DELETE 该资源,它不应改变任何东西。资源应该已经消失了。POST 是 REST 规范中唯一的非幂等方法,所以你可以多次 POST 同一个资源,你会得到重复项。
让我们重新审视 Slack 的例子,看看如果我们在其中使用 HTTP 动诖进行更多操作会是什么样子。
我们可以使用 POST 向 general 频道发送消息。我们可以使用 GET 从 general 频道获取消息。我们可以使用 DELETE 删除具有特定 ID 的消息——这变得有趣了,因为消息不与特定频道绑定,所以我可能需要设计一个单独的 API 来移除消息。这个例子展示了设计 API 并不总是容易的;有很多选择和权衡要做。
等级 3:HATEOAS
还记得只有文本的计算机游戏,没有任何图形吗?你只有很多描述你在哪里,以及你接下来能做什么的文本。要进展,你必须键入你的选择。HATEOAS 就有点像这样。
HATEOAS 代表“应用程序状态的超媒体引擎”(Hypermedia as the Engine of Application State)
当你有了 HATEOAS,每当有人使用你的API时,他们可以看到他们还可以用它做什么。HATEOAS 回答了“我接下来可以去哪里?”的问题。
但这还不是全部。HATEOAS 还可以对数据关系进行建模。我们可以拥有一个资源,URL 中不嵌套作者,但我们可以发布链接,所以如果有人对作者感兴趣,他们可以去那里探索。
这不像成熟度模型的其他级别那样流行,但有些开发者使用它。例如 Jira,下面是他们搜索 API 的一部分:
他们嵌套了你可以探索的其他资源的链接,以及这个问题的转换列表。他们的 API 很有趣,因为它在顶部有一个“扩展”参数。它允许你选择你不想要链接的字段,而是选择完整内容。
使用 HATEOAS 的另一个例子是 Artsy。他们的 API 严重依赖 HATEOAS。他们还使用 JSON Plus 调用规范,这为链接结构制定了特殊的约定。下面是使用 HATEOAS 进行分页的一个例子,这是使用 HATEOAS 的最酷的例子之一。
你可以提供指向下一个、上一个、第一个、最后一个页面的链接,以及你认为必要的其他页面的链接。这简化了 API 的使用,因为你不需要在客户端添加URL解析逻辑,或者添加分页号的方式。你只需得到已经结构化好的链接的客户端就可以使用了。
什么构成了一个好的 API
到此为止 Richardson 的模型,但这并不是构成好API的全部。其他重要的质量是什么呢?
错误/异常处理
我期待从我使用的 API 中得到的一个基本的东西是,需要有一个明显的方式来告诉我是否有错误或异常。我需要知道我的请求是否已处理。
瞧,HTTP 还有一种简单的方式来做到这一点:HTTP 状态码。
控制状态代码的基本规则是:
- 2xx 表示正常
- 3xx 表示你要找的公主在另一个城堡——你要找的资源在另一个地方
- 4xx 表示客户端做了一些错误的事情
- 5xx 表示服务器失败
- 500 内部服务器错误 - 小猫咪梗
至少,你的 API 应该提供 4xx 和 5xx 状态码。5xx 有时是自动生成的。例如,客户端向服务器发送某些东西,它是一个无效请求,验证有缺陷,问题沿着代码下发,我们有一个异常——它将返回一个 5xx状 态码。
如果你想要致力于使用特定的状态码,你会发现自己在想,“哪个代码最适合这种情况?”这个问题并不总是容易回答。
我建议你去查阅 RFC,它规定了这些状态码,比其他来源提供更广泛的解释,告诉你这些代码什么时候合适等等。幸运的是,有几个在线资源可以帮助你选择,比如 Mozilla 的 HTTP 状态码指南。
文档
伟大的 API 拥有伟大的文档。文档的最大问题通常是找人来更新它,随着 API 的增长。一个很好的选择是自我更新的文档,它与代码没有脱节。
例如,注释与代码无关。代码改变时,注释保持不变,变得过时。它们可能比没有注释还糟糕,因为过一段时间后它们将提供错误的信息。注释不会自动更新,所以开发者需要记得与代码一起维护它们。
自我更新文档工具解决了这个问题。一个流行的工具 Apifox 可以高效的帮助你解决问题。
可缓存性
在某些系统中,可缓存性可能不是大问题。你可能没有很多可以缓存的数据,一切都在不断变化,或者你可能没有很多流量。
但在大多数情况下,可缓存性对于良好的性能至关重要。它与 RESTful API 相关,因为HTTP协议与缓存有很多关系,例如 HTTP 头允许你控制缓存行为。
你可能希望在客户端缓存东西,或者在你的应用程序中缓存,如果你有一个注册表或值存储来保存数据。但 HTTP 允许你几乎免费获得良好的缓存,所以如果可能的话——不要错过免费的午餐。
此外,由于缓存是 HTTP 规范的一部分,很多参与 HTTP 的东西都会知道如何缓存东西:浏览器,它们天生支持缓存,以及你和客户端之间的其他中间服务器。
进化的 API 设计
构建 API 和现代软件的最重要部分是适应性。没有适应性,开发时间会减慢,尤其是在面对截止日期时,推出功能变得更加困难。
“软件架构”在不同的上下文中意味着不同的东西,但就目前而言,让我们采纳这个定义:
软件架构:避开阻碍未来变更的决策的行为/艺术。
考虑到这一点,当你设计你的软件并必须在具有相似好处的选项之间选择时,你应始终选择更具未来性的那一个。
好的实践并不是一切。以正确的方式构建错误的东西并不是你想要的。更好的是采纳成长的心态并接受变化是不可避免的,尤其是如果你的项目将继续增长的话。
为了让您的 API 更具适应性,其中一个关键做法是保持API层的轻便。真正的复杂性应该下放。
API 不应该决定实现
一旦你发布一个公共 API,它就是固定的,你不能更改它。但如果你别无选择,只能承诺一个设计得不够好的 API 怎么办?
你应该始终寻找简化实现的方法。有时,用一个特殊的 HTTP 头来控制你的 API 的响应格式可能是一个比构建另一个 API 并称之为 v2 更简洁的解决方案。
API 只是另一层抽象。它们不应该决定实现。有几种开发模式可以帮助你避免这个问题。
API 网关
这是一种外观模式开发模式。如果你将一个单体分解成一堆微服务,并想向世界公开一些功能,你只需建立一个 API 网关,它就像一个外观一样。
它将为不同的微服务(可能具有不同的 API,使用不同的错误格式等)提供一个统一的接口。
针对前端的后端
如果你需要构建一个 API 来满足几种不同的客户端,这可能会很困难。为一个客户做出的决策会影响其他客户的功能。
针对前端的后端说——如果你有不同的客户喜欢不同的 API,比如移动应用喜欢 GraphQL,那就为他们建立 API。
这只有在你的 API 是一个抽象层,并且很薄的情况下才有效。如果它与你的数据库耦合,或者太大,逻辑太多,你就无法做到这一点。
GraphQL 与 RESTful
GraphQL 有很多炒作。它是新来的,但已经吸引了许多粉丝。以至于一些开发者声称它将取代 REST。
尽管 GraphQL 相对于 RESTful 规范来说较新,但它们有很多相似之处。GraphQL 的最大缺点是可缓存性——它必须在客户端或应用程序中实现。有客户端库具备内建的缓存能力(如 Apollo),但这比利用 HTTP 提供的几乎免费的缓存能力更难。
技术上讲,GraphQL 处于 Richardson 模型的 0 级,但它具有良好 API 的特性。你可能无法使用几项 HTTP 功能,但 GraphQL 旨在解决特定问题。
GraphQL 在合并不同API并将它们作为一个 GraphQL API 公开时表现出色。
GraphQL 在处理欠抓取和过度抓取方面表现出色,这是 REST API 可能难以管理的问题。这两者都与性能相关——如果你欠抓取,你没有有效地使用 API 调用,所以你必须进行很多调用。当你过度抓取时,你的调用导致的数据传输比必要的更大,这是带宽浪费。
REST 与 GraphQL 的比较是一个很好的过渡,总结了一个好 API 的最重要特征。
好的API特性
- 你需要清晰表示数据——RESTful 通过资源的形式为你提供这一点。
- 你需要展示哪些操作可用——RESTful 通过结合资源与 HTTP 动词做到这一点。
- 需要有一种确认是否存在错误/异常的方法——HTTP 状态码可以做到这一点,可能还有解释它们的响应。
- 有可发现性和导航的可能性很好——在 RESTful 中,HATEOAS 负责这一点。
- 拥有出色的文档很重要——在这种情况下,可执行的、自更新的文档可以处理这个问题,这超出了 RESTful 规范的范畴。
- 最后但同样重要的是——伟大的 API 应该具备可缓存性,除非你的特定情况表明这不是必需的。
REST 与 GraphQL 之间最大的区别是它们处理缓存的方式。当你按照 REST 方式构建你的 API 时,你几乎可以免费获得 HTTP 缓存。如果你选择 GraphQL,你需要担心在客户端或你的应用程序中添加缓存。