为Web设计、实现和维护API不仅仅是一项挑战;对很多公司来说,这是一项势在必行的任务。 本系列 将带领读者走过一段旅程,从为API确定业务用例到设计方法论,解决实现难题,并从长远的角度看待在Web上维护公共API。沿途将会有对有影响力的人物的访谈,甚至还有API及相关主题的推荐阅读清单。 这篇 InfoQ文章是 Web API从开始到结束系列文章中的一篇。你可以在这里进行订阅,以便能在有新文章发布时收到通知。 有关于如何在API中使用超媒体方面的各种理论,能够找到的(电子)文字可谓不计其数,但是在实践方面的资源似乎有些匮乏?在近期举办的API Craft 2014大会上,许多演讲者为与会者分享了使用超媒体方面的故事,但他们都有些不好意思地承认,他们之前从来没有在公众面前谈论这些话题。因此,我们中的许多人决定要将我们的经验分享给大家。我也将在这里为读者分享我本人在这一主题上所遇到的四个示例。 在本文中,我们将讨论四种关于超媒体在真实情况中的实现:在图片链接中使用超媒体(你很可能已经使用过这种方式了)、GitHub是如何使用Link头信息实现分页的、在例如iOS这样的受限系统中使用超媒体,以及Balanced是如何使用超媒体理论开发产品的故事。这些故事分别描述了不同的场景,并且各自展示了在API设计中使用超媒体的不同方面。 虽然许多有关于超媒体的理论都将超媒体描述为整个API的基础结构与底层理论,但我还是要为你分享一个小秘密:这一点并非是必需的。即使不将整个API设计为完全使用超媒体,你依然能够从超媒体的使用中受益。我将为你分享两个最常见的案例:缩略图和分页。 你可能已经在无意中使用了某些超媒体技术了,我经常在包含了图片,尤其是缩略图的相关API中看到这一情况。请考虑一下以下这个来自于Twitter的API响应(节选): 没有人认为Twitter有一套完整的超媒体API,但以上这个响应中所包含的链接却确确实实地应用了超媒体技术。它本可以选择使用数据URI的方式内联包含这些图片,但它最终选择了使用链接的方式。 由于在响应中包含了链接地址,因此使用该API的客户端就能够自由选择要下载怎样的信息。这些链接告知了客户端有哪些选择,并且它们的地址在哪里。换句话说,这就是“货真价实”的超媒体应用。 为了让这种方式所带来的优点更明显一些,让我们来考虑一下这个略有不同的响应内容: 由于在这里应用了超媒体,因此我们无需返回三个不同版本的用户档案图片。我们所做的只是告诉客户端有三种可用的图片尺寸可以选择,并且告诉客户端能够在哪里找到这些图片。这样一来,客户端就能够根据不同的场景,做出符合自身需要的选择。而且,如果客户端只需要一种格式的图片,那就无需下载全部三种版本的图片了。这样一来可谓一箭三雕:既减少了网络负载,又增进了客户端的灵活性,更增进了API的可探索性。 我想说的是,或许你已经部署了一套简单的超媒体API,只是你之前从来没想到罢了。即使你没有将整套API都围绕着超媒体进行设计,你也能够从这个场景中获得好处。 只需简单地应用某些超媒体,就能够极大地简化客户端的代码,分页正是一个典型的场景。让我们看看GitHub这一现实世界中的示例。在GitHub的文档中,提到了它们对于API所采取的某种限制。 不同的API调用可能会返回不同的默认值。举例来说,如果调用了某个方法去列出GitHub上所有的公共代码库,那么所返回的数据集是每页30个项目;而如果调用了GitHub的Search API,那么会返回每页100个项目的结果。 让我们对照着响应的示例来看一下,这样能够更容易理解这些内容。来看一下GitHub是如何实现这一点的吧。 以下的示例是在使用GitHub的搜索请求时,所得到一个经过分页的结果集: 以上请求会返回带有Link头信息的响应: 由RFC 5988所定义的Link头,正如其名称所显示的一样,为客户端提供了链接。这些链接各自包含一个URL以及一个链接关系,其中的rel就代表了链接关系的内容。因为该响应返回的是结果集中的第一页内容,因此GitHub所返回的响应中包含了下一页及最后一页的选项。 如果我们继续访问下一个链接,就会得到一个不同的头信息集: 在这个响应中,我们会看到其中包含了一个前一页及第一页的链接。 那么这种方式的优点在哪儿呢?那就是客户端代码简化了。设想一下使用Ruby的场景,在传统的情况下,如果我们需要获取搜索结果的下一页,通常会这么做: 如果使用了超媒体,我们就可以这么做了: 不仅代码简化了许多,而且出错的机率也小得多。不仅如此,如果GitHub有朝一日决定将默认值改为每页10个结果,而不是当前的每页15个结果,那么第二个示例中的代码也无需进行任何改动。而第一个示例中的代码则不得不进行修改,并且如果代码不及时修改,你的用户就会在使用时遇到错误。 超媒体在iOS上的应用正在逐渐普及。原因在于:如果要在iOS应用上进行任何改动,你必须经历Apple的批准流程。但如果你使用了超媒体,那么服务端就能够改变客户端的行为。在很久之前,我和朋友们就在某个项目中使用了这一技术,恕我不能透露这个项目的名称,以保护这个可爱的小朋友不要碰到麻烦。;) 我们所创建的应用程序是一个音频播客应用,为用户提供大量的音频及视频文件。当时,Apple在音频方面有一个严格的限制:所提交的应用不允许在GSM连接环境下提供高品质的音频文件。不过,我们设计了一个方案以解决了这个问题,那就是链接。 当应用还处于审查阶段时,我们在服务器上只为该播客提供低品质的音频链接。一旦审查通过,我们就在服务器上提供了高品质的音频文件。我们的客户就能够免费升级,获得了超越技术限制所允许的高品质文件。是不是很无耻?哈哈! 当我们想到这个主意后,就立即想到可以在应用的其它部分也使用这项技术。举例来说,某些播客能够进行实时广播,并且允许你通过一通电话参与这档节目。在用户界面上,我们让应用发送一个对服务器的请求,以查询这档节目是否已经开始了。一旦节目开始,我们就会显示一个按钮,让你可以单击这个按钮以参与节目。只要你没有关闭屏幕,应用就会不断地向服务端的终结点发送请求,一旦节目结束,这个按钮就消失了。这种由服务端驱动的互操作性正是超媒体的实用方式,如果我们必须要部署一个新的客户端以改变按钮的状态,这个场景就不可能实现了。 继续来看下一个例子,某档播客节目的主办者可以提供该节目播出的时间。在屏幕上的信息一栏中就能够看到诸如“每个星期五中午”这样的说明。如果这些信息不是从服务器获取的,那么如果主办者打算改变节目播出的时间,用户就不得不更新应用,还得经历绕不过去的“等待审核”过程,才能够获得正确的信息。由于所有的档案都是由服务端驱动的,因此一旦节目的主办者点击了“保存”按钮,所有的应用都能够获得更新的信息。 以上这些和超媒体有什么关系呢。嗯,有两点关系。首先,服务端控制着所有的可能性,而客户端对这些可能性进行展示,这一种方式正是超媒体处理API设计的核心所在。以上所有三种应用场景都是对这一原理进行应用的绝佳例子。 其次,我们通过链接的方式实现了这三个场景。在应用刚刚启动时,会从服务端获取一个XML配置文件,其中提供了RSS的链接、“查看当前是否有节目在播出”的链接、以及对档案信息的链接。应用会随后在需要的时候使用这些链接,为了实现RSS的转换,我们只需将“低品质”的链接改为“高品质”的链接,客户就能够自动获得一套完全不同的RSS了。 我们还可以按照这个思路继续探索下去。在某些领域中,如果你无法频繁地对客户端进行更新,那么让客户端在不经过更新的情况下对服务端进行正确的响应就是重点所在。在类似于iOS的系统或嵌入式设备的场景下,这种限制是非常明显的,因为你的用户通常不会像你所期望的那样频繁地对客户端进行更新…… 最后,我想谈论一下我之前的公司——Balanced对超媒体的应用的某些情况。Balanced的API设计应用了超媒体技术,并且遵循了由我参与共同设计的JSON API标准。在之前我所提到的示例中,都是在响应中加入了少量的超媒体相关信息,但Balanced却是完全由超媒体所驱动的。这个产品的完成效果更好,但也同样遇到了许多挑战。 从好的方面来说,新特性的发布不会破坏已有的客户端的运行。举例来说,该API的1.1版本是首个完全遵循JSON API规格的版本。当1.1版本发布之后,Balanced又推出了一个Push to Card的特性,这是一个全新的功能。由于超媒体的存在,我们无需推出一个新的API 1.2版本,因为现有的客户端会自动忽略新的特性,而只有新版本的客户端才能够使用这一特性。这就大大简化了运营工作,因为管理多个版本会使部署及开发复杂化。只要Balanced继续将新的特性添加到API中,他们就会按照相同的办法进行处理。 超媒体的狂热支持者总是在谈论它的好处,但并非超媒体的每个特征都是正面的。站点Balanced的立场上,我也要提一提它的负面影响。通常在客户报告了某个与产品支持相关的问题时,你会问客户的第一个问题都是“您是哪位?”在许多情况下,这一问题具体表现为“你的客户ID是多少?”但由于Balanced使用了超媒体,因此客户没有ID号,他们只有一个客户URL。在有些时候,当你向客户问及他们的“客户URL”时,他们可能会感到困惑。随着更多的人理解超媒体API的概念,这种情况可能会逐渐发生改变。但由于目前超媒体的应用还属于新鲜事物,那么对于完全使用超媒体进行API设计的人来说,他需要做好准备,对使用他的设计的客户进行一些使用方面的指导。在Balanced的案例中,这就意味着他们要事先为客户传授一些知识,因为许多人还不知道如何开发一个良好的超媒体客户端。虽然说为客户提供一些预先发布的客户端作为参考是个好主意(在本例中,Balanced必须这么做),但如果使用更为传统的API,他们就能够将开发精力专注在其它有意义的事情上了。 如你所见,对超媒体的应用存在着多种形式,它并不一定要成为你的API的唯一的组织原则。首先,我们谈到了你或许在不经意间就通过图片链接的方式使用了超媒体。随后,我们谈到了GitHub以及他们的分页示例。接下来,我们通过一个示例表示一个由服务端驱动的客户端应用不需要频繁地进行更新,这对于类似于iOS的受限环境是非常有用的。最后,我们讲到了某个公司将超媒体的应用作为他们的竞争优势的案例,但这种应用也同时带来了一些负面效应。 希望这些真实世界中的超媒体实现能够让你理解,对于超媒体的应用并非那么绝对——要么完全使用,要么完全不用;并且你或许已经在某些场景中使用到它了。我很高兴地看到超媒体在越来越多的API中发挥作用,也乐于听到人们分享他们在应用这项技术时遇到的各种成败得失。 Steve Klabnik是一位Rails项目的成员,也是Rust项目的贡献者,同时也是《Rails 4 in Action》、《Designing Hypermedia APIs》及《Rust for Rubyists》等书籍的作者。 为Web设计、实现和维护API不仅仅是一项挑战;对很多公司来说,这是一项势在必行的任务。本系列 将带领读者走过一段旅程,从为API确定业务用例到设计方法论,解决实现难题,并从长远的角度看待在Web上维护公共API。沿途将会有对有影响力的人物的访谈,甚至还有API及相关主题的推荐阅读清单。 这篇 InfoQ文章是 Web API从开始到结束系列文章中的一篇。你可以在这里进行订阅,以便能在有新文章发布时收到通知。缩略图
GET https://api.twitter.com/1.1/users/show.json?screen_name=rsarver
{
"name": "Ryan Sarver",
"profile_image_url": "http://a0.twimg.com/profile_images/1777569006/image1327396628_normal.png",
"created_at": "Mon Feb 26 18:05:55 +0000 2007",
"location": "San Francisco, CA",
"profile_image_url_https": "https://si0.twimg.com/profile_images/1777569006/image1327396628_normal.png",
"utc_offset": -28800,
"id": 795649,
"lang": "en",
"followers_count": 276334,
"protected": false,
"profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/113854313/xa60e82408188860c483d73444d53e21.png",
"verified": false,
"time_zone": "Pacific Time (US & Canada)",
"description": "Director, Platform at Twitter. Detroit and Boston export. Foodie and over-the-hill hockey player. @devon's lesser half",
"profile_background_image_url": "http://a0.twimg.com/profile_background_images/113854313/xa60e82408188860c483d73444d53e21.png",
GET https://api.example.com/profile
{
"name": "Steve",
"picture": {
"large": "https://somecdn.com/pictures/1200x1200.png",
"medium": "https://somecdn.com/pictures/100x100.png",
"small": "https://somecdn.com/pictures/10x10.png"
}
}
分页
GET "https://api.github.com/search/code?q=addClass+user:mozilla"
Link: ; rel="next",
; rel="last"
Link: ; rel="next",
; rel="last",
; rel="first",
; rel="prev"
require 'uri'
url = "https://api.github.com/search/code"
per_page = 15
current_page = 1
next_page = 1 # zero based, of course
page = (current_page + next_page * per_page).to_s
query = "addClass+user%3Amozilla"
uri = URI(url)
uri.query = URI.encode_www_form([["q", query], ["page", page]])
puts Net::HTTP.get(uri);
# response contains parsed body from previous request.
puts Net::HTTP.get(response.headers[:link].rels[:next])
iOS超媒体
案例学习:Balanced
结论
关于作者