API 中的版本控制

2025-06-08

API 中的版本控制

1991年的圣诞夜,世界各地的孩子们都兴奋不已,许多成年人也热情高涨。

但马克·克里斯宾却不这么认为。相反,他向“ietf-822”邮件列表发送了一封详细的电子邮件,详细说明了新提出的MIME-Version标头字段存在哪些问题。他说,目前的形式几乎无法使用。他还问道,如果收到与 不同的版本,该怎么办1.0?“这是什么意思???”他罕见地用全大写字母问道。

最后他说,如果社区需要更改版本,他们最好制定一个全新的协议 - 而且他会坚决反对任何仅仅更改版本号的举动。

马克的咆哮

MIME简史

这里需要绕行一小段路。

MIME,或更正式的说法是多用途互联网邮件扩展(或“多媒体”;像许多 IETF 首字母缩略词一样,扩展名随着时间的推移而发生了变化)是在 20 世纪 90 年代初创建的,用于替代通常任意的“改进”互联网邮件的方法。

与之竞争的提案,例如 X.400,提供了比互联网纯文本更丰富、更复杂的消息格式。当时 Unicode 的概念尚未出现,但不同字符集及其不同表示方式的概念已经出现——但互联网邮件仅支持 ASCII。专有电子邮件协议(其中有很多)也支持丰富的多媒体消息传递。更广泛的社区引入了诸如“uuencode”和“binhex”之类的激动人心的功能来弥补这一缺陷。

因此,在整个 20 世纪 90 年代初期,人们做出了一致努力来扩展互联网邮件,以包含所有这些类型的富消息传递 - 格式化的文本、图像和附件。

大约四五年后,当我开始使用电子邮件时,MIME 已经成为了热门的新兴事物,我们中的许多人不得不费力地学习如何通过目测 Quoted Printable 来阅读消息。

MIME 后来成为了网络多媒体的基础——HTTP,尽管最初被称为“超文本传输​​协议”,但更确切地说应该叫“MIME 传输协议”。作为网络和电子邮件(迄今为止使用最广泛的两个应用层协议)的核心,MIME 完全可以称得上是历史上使用最广泛的设计。

如今,距离 MIME 的设计已经过去了将近三十年——整整三十年。或许现在下结论还为时过早,但看起来马克确实是对的。

仍然必须包含MIME-Version: 1.0

协议和 API

好的,到目前为止,我提到了“版本”,但没有提到“API”。

我们年轻的时候,把 API 称为“协议”。如今,这个词似乎只用于描述栈底层的东西。然而,设计原则始终如一。适用于 HTTP 甚至 IP 的设计原则,同样适用于你的博客 API。

所以请原谅我有点老套,任何时候你看到“协议”,只要它让你更开心,就把它读作“API”。

一个全新的世界

我承认,我最近没怎么用电子邮件。但我敢打赌,尽管 MIME 版本是强制要求的,但没人会检查。就像马克多年前说的,如果检查失败了,你还能怎么办?

Mark 建议,由于对 MIME 版本的任何更改都将构成重大变更,因此最好的解决方案是制定一个全新的协议。由于 MIME 被定义为一组标头字段,因此您只需使用不同的字段即可。MIME 使用(MIME-Version至少除了 之外)一组以 开头的标头字段Content-。Mark 建议我们必须改用类似Body-“彻底彻底”的方案。

这样做的结果是,你并没有从任何语义上“更改版本”,而是完全“更改了协议”。有时候,解决这个问题的最佳方法确实是在协议后加上任意数字——但不要自欺欺人地认为这些就是“版本”这个词的任何实际含义。

因此,如果你的协议已经扎根/blogging/v1/,现在又想修改一些关键的东西,/blogging/v2/那你就得把所有东西都改了——所有老客户端都会崩溃(或者继续使用旧协议,甚至可能永远)。期望所有客户端和服务器都同时更新,这被称为“叉车升级”——因为这些东西已经嵌入到硬件中,你需要进行物理上的修改——这通常被认为是一件坏事。

扩展

眼尖的朋友们肯定注意到,MIME 并没有停止使用Content-Type和朋友。然而,在 1992 年原始 RFC 发布之后,MIME 的开发并未停止。之后,新的 RFC 又发布了两次——分别在 1993 年和 1996 年,并且 MIME 在 1997 年开发了全新的功能,例如传递演示信息。

这些都依赖于第一条规则:

  • 接收者会忽略他们不理解的额外内容。

用 JSON 术语来说,如果你从 API 返回的对象包含一些对你来说不熟悉的键,不必担心,它们并不重要。

许多 X.nnn 协议及其衍生协议对此做了一些修改,允许发送者指定某个扩展是否为“关键”扩展。如果无法理解“关键”扩展,则意味着整条消息无法理解,并会产生错误。非关键扩展则可以直接忽略。

IMAP,即 Mark Crispin 的互联网邮件访问协议,基于不变的第一规则构建。服务器可以添加数据(在特定位置——IMAP 的许多结构都是位置相关的),而不会产生不良影响。IMAP4rev1 自 1996 年开始使用。

XMPP 完全基于规则一构建。客户端和服务器经常会发送它们无法理解的流量。事实上,服务器通常也无法理解它们在客户端之间转发的所有流量。XMPP 自 1999 年以来一直在使用(目前仍为 1.0 版本)。

事实上,“端到端原则”是规则一的一个具体案例——非端点的接收者只需传递未改变的数据。

HTTP 的历史可谓跌宕起伏——HTTP/0.9 和 HTTP/1.0 的设计都脱离了通常的专家团队,因此也遭遇了诸多问题。HTTP/1.1 在很大程度上与 HTTP/1.0 交叉兼容,但也修复了大部分问题。HTTP/2.0 则进行了彻底的重新设计。HTTP/1.0 中的版本协商机制实际上从未在实践中使用过——所有更改都是通过规则一或协商机制进行的。

谈判

有时,知道接收方是否能够理解你发送的流量是件好事。有时,你希望接收方能够返回额外的数据。

对此的解决方案仍然可以是一个版本 - 但同样,它是一个叉车,一个全有或全无的二进制开关。

相反,要保持交换机的规模小。IMAP 开创了一种模型,服务器发布“功能字符串”,客户端会解析这些字符串,然后协商特定的选项,或者仅仅知道它们可以使用某个功能。你可以将这些协商视为显式或隐式协商——使用哪种方式取决于功能的复杂性以及你能承受的往返次数。

同样,在 XMPP 中,任何实体(客户端或服务器)都可以询问其他实体其支持哪些协议,然后进行显式或隐式协商。不过,很多时候,“先用着看看是否有效”的策略就足够了——如果你愿意,可以称之为一种单边隐式协商——或者“先试试再说”。

基于 HTTP 的协议(例如 REST API)不太适合复杂的协商,但规则一和隐式协商都能很好地工作。概览端点可用于提供服务器端功能和初始端点,请求可以使用标头字段或参数来指示支持的功能并请求使用它们。当然,您也可以借用 X.nnn 的“关键扩展”。

如果操作正确,各个服务器和客户端可以完全独立地升级。

千万不要以为因为它是 HTTP,你的客户端代码就会从你的站点下载,所以这无关紧要 - 如果它是一个公共 API,那就不是这样,如果它是一个私有 API,如果您的网站不错,人们仍然会一直打开他们的浏览器。

再次出现版本

现在,你拥有的不再是一个单一的、版本化的协议,而是几十个小型的、专用的协议。举个简单的例子,一个博客协议可能有一个用于帖子的协议,另一个用于评论的协议。帖子可能包含一组内容部分,而图像可能与文本完全不同。

通常,使这些协议适应新需求的最佳方法是添加新功能,要么遵循规则一,要么根据需要协商。但是,全新扩展的成本远低于全新协议的成本,而且由于“命名”仍然是计算机科学中最难的两大问题之一(另两个是缓存失效和差一错误),因此使用简单的版本号来命名协议确实很有意义。

在 XMPP 中,协议扩展通过 XML 命名空间工作——简单来说,我们用 URI 来命名协议。对于官方扩展,我们使用包含版本号的 URN 形式。每个新版本都是一个完全不同的协议——例如,能够理解“urn:xmpp:mam:0”的客户端根本无法理解“urn:xmpp:mam:1”——但服务器可以轻松同时支持这两种协议,并且在过渡期间通常会这样做。

很多时候,我们能够以一种不强制“命名空间冲突”的方式整合变更。许多协议扩展在“版本 0”上整个开发周期都存活下来。有时,这其实是由于严重滥用 XML 命名空间造成的——我们随意更改 XML 模式,尽管我们明知在实践中一切正常。但偶尔,我们需要做出重大改变。

因此,尽管即使在扩展级别也值得避免更改版本,但至少如果您确实需要这样做,那么如此细粒度的更改很有用。

但请记住 - 这些与仅仅使用一个不同的、全新的协议没有什么不同。对于 REST API,将“v1”更改为“v2”与以任何其他方式更改端点 URI 没有任何区别。

但它们不是版本

真正的版本 - 就像现在所谓的“语义版本控制”(并且已经用于soname版本控制多年)包含有关向后兼容性等各种信息。

这些对于软件库版本控制非常有用和有效,并且人们很容易认为该模型也适用于协议。

但事实并非如此。

MIME 的证据是,成功的、设计良好的、可扩展的协议永远不会改变版本 - 而协议的可扩展性和功能协商可以使简单的设计持续数十年。

在过去的几十年里,具有功能协商功能的可扩展协议模型已经经过了多次开发和完善。作为一个概念,它始终经受住了时间的考验。

当然,你设计的协议或 API 可能不会持续数十年。但正如即使你不打算维护软件库数十年,也应该使用语义版本控制一样,你也应该理所当然地设计可扩展的协议。

鏂囩珷鏉ユ簮锛�https://dev.to/dwd/versioning-in-apis-22bj
PREV
可以使用 DeepSeek R1 或 Visual Studio Code 或 Cline 或 Roo Code
NEXT
重温 Git 子模块