2017 年年末,就职小米的一位前同事送了我一枚 F 码,我用它抢购到一枚小爱音箱。我满怀期待地装上“小爱同学”,希望能够通过她用语音控制所有小米产品。但我失望地发现,早先我购买的 YeeLight 床头灯并不能接受“小爱同学”的指挥——YeeLight 床头灯的客户端只能适配其出厂时的服务端所提供的功能,在服务端扩展新的功能,比如接入智能语音控制之后,已经发布的客户端却无法跟上,无法获得新功能。
随着互联网尤其是物联网的发展,跨企业、跨行业的互联需求越来越多,上述遗憾会频繁发生,无疑会造成巨大的浪费,也阻碍了物联网的发展。我们迫切需要服务端和客户端相互独立演化的能力,这正是 RESTful 服务端与客户端的用武之地。
本质上,服务端和客户端独立演化的能力取决于双方的实现细节的解耦程度。首先,如果服务端与客户端紧密耦合,服务端变更就很可能造成客户端失效。例如,服务端和客户端通过 RPC 的 IDL 来约定服务接口,那么当服务端的变更引起过程名、参数或返回值的变更时,客户端就很难不受其影响。又例如,服务端的 Web Service 遵循一套 URLtemplate 规约,客户端根据服务端提供的规约文档编码,如果服务端改变了资源名称或资源之间的关联关系,也会导致客户端失效。其次,与服务端紧密耦合的客户端显然也无法正常接纳服务端推出的新功能。
REST 解决的思路就是将可能变化的部分抽象出来,融入服务端响应的消息体中,如果客户端拥有解读这些变化的能力,也就使双方都获得了独立演化的能力。
在过去我们非常熟悉的“通用浏览器 服务端渲染逻辑(CGI、JSP、ASP、PHP 等) HTML”方案中,服务端输出了整个 HTML 文档,自然也包含了服务端所有可能变化的部分。从理论上说,客户端和服务端的确获得了相互独立演化的能力——浏览器是通用的,不需要应用程序员编写任何客户端代码,也并不因为服务端改变了 JSP 脚本或增加一个功能,就需要升级浏览器。这是一个极为流行的方案,成功地挤掉了 C/S 结构的份额,成为当时互联网应用的主流方案。
然而,这个服务器渲染的方案并不够好。因为服务端响应是完整的 HTML 文档,不仅有业务数据,而且包括渲染的每一个细节,客户端要么全要,要么全不要。如果客户端希望对业务数据再加工,或者局部渲染,就不得不剥离易变的其他元素,例如 <p/> 这个渲染相关的元素。写过爬虫的程序员应该深有体会,好不容易把有用数据从 HTML 大杂烩里提取出来,结果目标网站稍微改动一些风格布局,原先的爬虫程序就失效了。由于难以局部渲染,人们不得不忍受 HTML 页面跳转的糟糕体验。AJAX 和 JS 客户端渲染脚本的兴起成为压倒骆驼的最后一根稻草,传统的服务器渲染方案被逐步取代了。
拥有 JS 渲染脚本和静态 HTML 模板的富客户端,将服务端渲染方案中 view 的职责从服务端抢了过来,服务端只输出数据,不必带上各种 HTML 标签。职责的分离大大促进了复用,一个服务端可以拥有多个不同展现形式的客户端,一个客户端可以mashup多个服务端,跨企业的应用互联变得很流行。但这样一来,过去 C/S 结构的老问题——服务端与客户端紧耦合的毛病又出现了,如果客户端团队与服务端团队不是同一拨人,独立演化造成不兼容就会成为大问题。
客户端渲染和客户端服务端独立演化能否兼得?
- Mike Amundsen首先讲到,如果服务端响应中包含下一步操作的所有可能路径,客户端应该能够正确理解和处理这些路径代表的操作,人作为交互模型的主动角色,通过操控客户端选择路径,形成“探索地图”式的人机交互形式。其中客户端是不需要事先知道服务端提供的操作路径的,这就形成了服务端和客户端对于操作路径的独立演化的能力,换言之,服务端可以修改、增加、删除这些操作路径而不会引起客户端失效。
- 然后,他将类似“操作路径”这样的客户端需要识别的变化部分归纳并抽象为OBJECT、ADDRESS、ACTION(简称 OAA)三个重要元素,客户端需要理解响应中携带的OAA 的元数据,才能有效处理 OAA 元素的变化。这些元数据如下。 ■ OBJECT 元数据 :服务端返回业务数据的元数据,例如对象中字段的类型、长度甚至语义等,这些元数据能够帮助客户端正确地渲染展现数据。 ■ ADDRESS 元数据:响应中业务数据的关联操作列表,包括名字、URL、展示名称等,能够指示客户端展现当前可操作的最新功能。 ■ ACTION 元数据:对 ADDRESS 列表每一项操作的详细描述,包括参数的元数据(与OBJECT 字段元数据类似)、名字、URL、HTTP 方法、展示名称等,能够指示客户端引导用户正确地发起这个操作。
- 接着,讲述了如何在服务器响应中包含 OAA 这些可变因素,以及如何通过一些表述格式让客户端理解和正确处理 OAA。难得的是,Amundsen并没有假设理想化从头开始的场景,而是让虚拟主人公 Bob 和 Carol 从老团队和传统的设计中,将传统的个性化定制客户端一步一步重构和扩展为可复用的支持多种表述格式的通用客户端。
- 最后,Amundsen还讲述了处理版本化和微服务需要注意的地方。
RESTful Web 客户端是整个 REST 风格应用(服务端和客户端)的一部分,服务端已经有不少著作,《RESTful Web Clients :基于超媒体的可复用客户端》则是 HATEOAS 的RESTful 应用缺失的最后一块拼图,对于实践 RESTful 服务和客户端有重要的指导意义。
特别提醒读者在阅读过程中留意几点,第一,如何将系统中稳定不变的部分、较稳定不易变的部分和经常变化的部分相互分离,分别在服务端、客户端、通信报文中描述,以期获得最大复用的同时促进服务端和客户端的独立演化的。第二,从服务端渲染方案的缺陷我们可以得出结论,客户端并不是越通用越好。当缩小复用范围、聚焦某个行业领域时,保留客户端与服务端之间对该行业领域语义的耦合和隐式的规约,反而能够增强在领域内的复用程度而不影响行业领域内的复用。
展望未来,RESTful 运动可能会产生若干包含协议语义的通用格式与具体应用领域 DSL 相结合的应用模式。最后,留一个待解决的问题,即当客户端不是由人类,而是由另一程序操控时,地图将退化为规则操控的路径,成为自动化的客户端 ;当这个规则是 AI 操控时,这个客户端将进化为智能客户端,这时,YeeLight 将不仅能接受“小爱同学”的指挥,而且能胜任更多预先并不知道的工作。
————
本文摘自新书《RESTful Web Clients:基于超媒体的可复用客户端》。本书将带领读者开启一段从个性化定制实现到强大的通用客户端应用的旅程,同时向你展示如何利用那些支撑万维网良好运行的基本原则。本书包括以代码为中心的章节信息和对相关重要主题的探索,比如表述器模式、人机交互模型和 Web API 在版本控制上的挑战等。直击下方阅读原文,开启架构全新境界,体悟开发深度思辨。(想进一步了解本书渊源,建议扫码识别下方李锟老师这篇包含深情与卓见的序文)
内容简介:Web开发领域的REST运动已经进行了很多年了,在REST的Richardson成熟度模型提出后,第3级——HATEOAS的应用——仍然没有得到广泛应用。事实上,其中一个难点在于客户端如何支持HATEOAS。之前很多REST相关书籍聚焦于如何打造服务端的RESTful API,本书则着重研究RESTful客户端,介绍了如何把一个针对服务端规约硬编码的定制客户端重构为一个支持HATEOAS的通用客户端,并提供了多格式支持、超媒体类型、版本化、微服务等相关问题的全面指导。本书附有所有样例代码的GitHub地址,方便读者快速理解和实践。本书适合Web应用开发者,尤其适合希望Web应用程序的服务端与客户端能够独立演化的Web架构师。