超媒体客户端

Carson Gross

通常,当我们在关于RESTHATEOAS在线讨论中令人难以忍受地吹毛求疵时,我们会这样说:

JSON不是超媒体,因为它没有超媒体控件。

看看这个JSON:

{
 "account": {
   "account_number": 12345,
   "balance": {
     "currency": "usd",
     "value": 50.00
   },
   "status": "open"
 }
}

看到没?没有超媒体控件。

所以这个JSON不是超媒体,因此返回此JSON的API不是RESTful的。

对此,偶尔会有聪明且经验丰富的Web开发者这样回应:

好吧,REST专家,那这个JSON呢?

{
  "account": {
    "account_number": 12345,
    "balance": {
      "currency": "usd",
      "value": 50.00
    },
    "status": "open",
    "links": {
      "deposits": "/accounts/12345/deposits",
      "withdrawals": "/accounts/12345/withdrawals",
      "transfers": "/accounts/12345/transfers",
      "close-requests": "/accounts/12345/close-requests"
    }
  }
}

现在,这个响应中有超媒体控件了(顺便说一句,正常人称之为链接),所以这个JSON是超媒体。

所以这个JSON API现在是RESTful的了。感觉好点了吗?

😑

必须承认,至少在高层面上,我们的在线对手有某种论点:这些看起来确实是超媒体控件,而且它们确实出现在JSON响应中。那么,难道不能称这个JSON响应是RESTful的吗?

由于天性固执,没有一两个"实际上"的辩驳,我们仍不愿立即让步:

等等:这种吹毛求疵正是互联网上关于REST的技术口水战如此特别有趣的原因。

然而,这里有一个更深层的"实际上",它不涉及JSON API本身,而是涉及线路的另一端:接收JSON的客户端

超媒体客户端与呈现信息

这个针对非RESTful JSON API的提议的深层问题在于,要让这个JSON响应能正确参与超媒体系统消费JSON的客户端也需要满足RESTful架构风格对整个系统施加的约束

具体来说,客户端需要满足统一接口,其中客户端代码除了能够向用户显示给定的超媒体外,对响应的"形状"或细节一无所知。在正常运行的RESTful系统中,客户端不允许拥有任何关于特定超媒体表示所代表领域的"带外"知识。

让我们再看一下提议的JSON即超媒体:

{
  "account": {
    "account_number": 12345,
    "balance": {
      "currency": "usd",
      "value": 50.00
    },
    "status": "open",
    "links": {
      "deposits": "/accounts/12345/deposits",
      "withdrawals": "/accounts/12345/withdrawals",
      "transfers": "/accounts/12345/transfers",
      "close-requests": "/accounts/12345/close-requests"
    }
  }
}

现在,这个API的客户端可以使用通用算法将此JSON转换为HTML。它可以通过客户端模板语言实现,例如迭代JSON对象的所有属性。

但有个问题:请注意JSON响应中没有太多呈现信息。这是所讨论账户的相当原始的数据表示,外加几个额外URL。

想要满足REST统一接口约束的客户端没有多少关于如何向用户呈现此数据的信息。因此,客户端需要采用非常基础的方法来显示此账户。

最终可能大致是一组名称/值对和一组通用的操作按钮或链接,对吧?

在保持对JSON响应形式不可知的前提下,它几乎无法做得更多。

推进我们的JSON API

我们可以通过使JSON API更精细来解决这个问题,开始包含更多关于如何布局信息的内容:或许指示某些字段应强调或隐藏等。

但这只是故事的一部分。

我们还需要更新客户端以正确解释JSON API的这些新元素。因此我们不再仅仅是API设计者:我们也进入了超媒体客户端创建业务。或者更可能的是,我们要求API客户端也进入超媒体客户端业务。

现在,Mike Amundsen写了一本优秀的书讲述如何构建合适的通用超媒体客户端。但在那本书中你会看到,创建好的超媒体客户端并非小事,而且这肯定不是大多数工程师为消费JSON API所构建的内容,即使JSON API在其响应中包含日益精细的超媒体控件和呈现信息。

"低效"表示

当我们开始考虑向JSON响应添加更多信息时,Roy Fielding论文中的一句话跃入脑海:

然而,这种权衡在于统一接口降低了效率,因为信息以标准化形式传输,而非特定于应用需求的形式。

-Roy Fielding, https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_5

对HTML的一个批评是它将"呈现"信息与"语义"信息混合。这常与典型JSON API响应的简洁性形成不利对比。

然而事实证明,正是这些呈现信息,以及Web浏览器(即超媒体客户端)将其转化为人类可交互UI的能力,使HTML作为更大超媒体系统(即Web)的组成部分如此有效。

而这正是我们发现自己要添加到JSON API中以支持合适超媒体客户端的内容。

构建超媒体客户端

所以你看,仅仅在JSON API响应中提供超媒体控件是不够的。它是REST故事的一部分,但不是整个故事。而且,我已经理解到,它实际上不是故事的难点。事实上,创建超媒体客户端才是难点,而创建好的超媒体客户端是真正的难点

现在,我们都习惯了Web浏览器的存在,但请花点时间想想,仅仅为了在普通日常Web请求中将HTML解析并渲染给最终用户,其中涉及的所有技术。这极其复杂。

这就是为什么,如果我们想要构建基于Web的超媒体驱动应用,使用标准的、基于Web的超媒体客户端:浏览器,可能是个好主意。

它已经是一个极其强大、经过充分测试的超媒体客户端。而且,稍加帮助,它可以成为更好的超媒体客户端。

总的来说,构建满足REST所有约束的良好超媒体客户端很难,我们应倾向于使用(并扩展)现有客户端,而非构建自己的新客户端。

Hyperview

话虽如此,有时构建新的超媒体客户端是合适的。例如,这正是Hyperview技术如此令人印象深刻和特别的原因。Hyperview不仅提供了一种新的、移动友好的超媒体规范HXML

它还向开发者提供了理解如何渲染HXML的超媒体客户端

没有这个超媒体客户端,Hyperview将只是另一个理论上的超媒体,就像上面的JSON一样,而非一个引人注目、实用且完整的RESTful超媒体解决方案。

没有超媒体客户端的超媒体就像没有自行车的鱼,只不过这条鱼其实只擅长骑自行车。

结论

我花了很长时间才理解客户端对于合适、RESTful的超媒体系统有多么重要。这是可以理解的,因为早期关于REST的大部分讨论都围绕API设计,而客户端很少被提及。

我现在看到的是,很多这些讨论是本末倒置:RESTful超媒体API只有在被合适的超媒体客户端消费时才有用。否则,你的超媒体控件就浪费在一个最终只是想要完成任务的领域特定厚客户端上了。

此外,你的超媒体API几乎肯定需要携带大量呈现层信息才能使整个系统可用。事实证明,Richard成熟度模型的"第3级"(超媒体控件)不足以达到"REST的荣耀"。

在实践中,你需要添加大量实用的呈现层技术来使你的超媒体API真正工作,而且你需要一个构建得当的超媒体客户端来消费它。

我在写HATEOAS是为人类服务的时已有这种初步意识,但当时我并未完全理解客户端/Web浏览器的特殊性。

REST不仅仅是关于API:正如Roy Fielding在其论文中明确指出的,它是一种系统架构。

除了像Mike这样的少数人,我们很大程度上忽略了REST故事中更大(真的大得多)的部分:

创建超媒体客户端很难(笑话)
</>