做为架构风格的 REST 究竟是什么

不少人搞不明白 REST(Representational State Transfer 表述性状态转移)缘由在于一开始就是把它当作设计风格而不是架构风格来理解,于是一上来就大谈特谈什么 RESTful API,结果是只见树木不见森林。html

仅从设计的角度去理解 REST(仅把它做为 API 设计原则),最多仅能理解其资源、表述这些概念,却很难理解状态转移究竟是怎么回事。web

要想搞清楚 REST,必须透彻理解三个关键概念:资源、表述、状态转移编程

REST 架构风格提出者和 HTTP 1.1 规范主要设计者都是同一我的 Roy Fielding。事实上,HTTP 1.1 正是 REST 风格的实现,于是认识 REST 最好的方式是从基于 HTTP 的 Web 应用开始。json

场景:

咱们看一个典型场景。浏览器

李小四想在京东上买一部 iPhone。服务器

首先他在浏览器地址栏输入 www.jd.com(固然也能够经过搜索引擎进入),打开京东商城首页,而后在首页搜索栏输入“iPhone”,回车,页面切换到含有 iPhone 关键字的商品列表。微信

李小四用鼠标点击其中一个商品,进入该商品详情页。架构

李小四看了看介绍,以为中意,因而选定颜色、型号、规格、数量,点击“加入购物车”,再点击“去购物车结算“,填写收货人信息、支付方式、开票信息,点击“提交订单”,选择一种支付方式支付并完成订单。app

李小四这我的性子比较急,下了单后,每隔一段时间就点开“个人订单”,点开物流信息看看手机到哪了。框架

终于,手机送到了,李小四从快递员那里签收后,京东立马经过微信给他推送一条货物签收通知,而且附上开票连接。李小四点击进入开票页面,获取一张电子发票。

资源及其表述:

在整个购物过程当中,李小四与之交互的是一个叫“京东商城”的 Web 应用——这是 REST 的做用对象。做为架构风格的 REST,其做用对象是一个完整的应用(或者系统)——确切地说是异构的分布式应用——而不是某一两个 API。这样的视角是理解 REST 全貌的关键。

李小四是如何获取到他想要的信息的?跑到卖家仓库去看实体 iPhone?若是这样,就没有 Web 什么事了。李小四在浏览器地址栏输入了一串叫 URL 的东西,而后浏览器就显示出京东商城首页了。

到底什么是资源?

本例中,真正的资源是 iPhone、物流、发票、钱等,但在谈论 Web 的时候,咱们说的资源通常不是指这些真正的实物资源,而是指存储在服务器上的特定数据,如这里的 iPhone、订单、物流、发票、帐户的数据信息

对于实体 iPhone,咱们能够去专卖店看看摸摸,那 Web 上的 iPhone 数据,咱们如何找到它,又如何看如何摸呢?

前辈们设计了个伟大的东西叫 URI,你每台 iPhone 不是有惟一编号嘛,那 Web 上这些虚拟的数据咱们也以虚拟物品(资源)的方式给它作惟一编号(标识)。虽然是由资源(虚拟数据)的拥有者来给它作标识,但为了统1、通用,前辈们对资源标识作了一些约束(协议),就造成了统一资源标识符(Uniform Resource Identifier,URI),这样便解决了如何找到资源的问题。好比 URL 经过 schema、域名、端口定位到服务器(资源拥有者),服务器内部再经过 path 和其余参数找到并处理资源。

从 URI (URL 是 URI 的一种实现方案)的定义看,它自己就是用来表达资源的,天生就是名词特性,只是在实际使用过程当中不知为啥就跑歪了,各类 /pathto/getuserinfo 动词性的 URL 满天飞(我的认为是成也 HTTP 动词,败也 HTTP 动词,更详细的分析见后面)。

资源是找到了,但咱们如何跟它交互呢?

若是是在本机,咱们能够经过程序直接操做资源(如经过程序指针直接操做内存数据),但 Web 是个分布式环境,指针没那么长,够不到对方的内存怎么办?

因而咱们须要在本地(客户端)拥有一份资源的副本。在 C/S 架构中,只有服务器拥有资源自己,其它客户端拿到的都是副本,并且拥有者(服务器)能够决定提供什么样的副本给客户端(提供哪些信息、以什么样的格式提供信息)。

这种带特定格式的资源副本就是资源的表述

做为资源拥有者,服务器固然能够决定提供什么样的表述形式,但正如 URI 同样,若是没有你们都承认的、通用的、被普遍支持的格式,服务器们各说自话,互相语言不通,那万维网恐怕就会成为巴比伦塔了。

因而前辈们又定义了一些通用的资源表述格式,官方话叫媒体类型。Web 上用的最普遍的媒体类型应该是 text/html,其余还有 image/jpeg、application/json、text/xml 等。

一个资源能够有多种表述(多种媒体类型),也就是说客户端(如浏览器)经过一个 URI(如 URL )能够得到该资源的多种表述中的一种。那么客户端和服务器端是如何沟通以在表述形式上达成一致呢?

在 HTTP 中,经过头信息协商。HTTP 有一系列 accept 请求头就是用来干这事的,如 accept、accept-encoding、accept-language。好比 accept: image/webp,image/apng,image/* 告知服务器“我能处理这些媒体类型,你给其中任意一种给我就行”。服务器端响应头 content-type 则告知浏览器该资源表述的确切媒体类型,如 content-type: image/jpeg 表示它是一张 jpeg 格式的图片。

另外,和现实世界同样,Web 上的资源具备集合特性,好比 iphone,并非指某一个 iphone,而是指 iphone 集合。从中咱们得出如下推论:

  1. 用来表示资源的 URI 应该使用名词复数形式;
  2. 对应集合的包含关系(集合中包含子集合),资源具备层级性;
  3. 集合中的元素具备集合范围内的惟一标识,经过在 URI 中带入该惟一标识来定位集合中的元素,如 /iphones/123456。

假设有这样一个 url:http://www.jd.com/mobiles/iphones/123456

首先这里体现了资源的层级性:手机是一个大资源集合,其下包含了 iphone 这个子集合,而经过资源标识 123456 定位到某一个 iphone。

那么,浏览器访问这个 url 时会返回什么呢?

首先取决于服务器端决定提供哪些媒体类型,咱们假设服务器端提供了 text/html、application/json、application/xml 和 image/jpeg 类型。

浏览器会决定请求什么类型呢?

若是咱们在地址栏输入该 url,浏览器通常会发送以下头部:accept: text/html,... 要求返回 html 文本。但若是咱们在 标签里面写该 url,浏览器会发送诸如 accept:image/* 要求返回图片格式——也就是说,取决于咱们在哪里用这个 url,这是浏览器的工做机制,也是 HTML 的魅力所在(后面分析超媒体时再详细分析)。

你可能会发现,现实中咱们见到的多数不是这样,更多是这样:

当要访问 html 类型时:http://www.jd.com/mobiles/iphone.html?id=123456(或者是编程语言后缀)

当要访问图片时:http://www.jd.com/mobiles/iphones/123456.jpg

现实中,咱们不但在 URL 中写入动词来表达要进行的操做,还写入类型后缀来表达要什么样的媒体类型——这二者都违背了 URI 和 REST 设计初衷,让 URI 这个标识符同时承担了操做和媒体类型,对外暴露了设计细节,且该 URI 只能用于极其狭隘的特定场景,违背了可扩展性设计原则(没法给该 URI 扩展更多的操做能力,也没法扩展其表述能力)。

如今咱们知道如何定位资源和如何传递(展现)资源,接下来的问题是,客户端如何操做资源呢?客户端没法经过操做表述(资源副本)改变资源状态,必须经过和服务器端交互来实现。

在 HTTP 中是经过几个通用的动词来表达客户端的操做意图的,最典型的 CRUD,对应 HTTP 动词(Method)POST、GET、PUT/PATCH、DELETE
状态转移:

经过 URL 定位资源,经过 HTTP 动词操做资源,经过状态码表示操做结果——如今大部分声称 RESTful API 的也都是作到了且仅作到了这些,大部分分析 REST 的文章也是到此便结束了,但实际上这只是开始。

相比于资源表述,REST 中更重要的第二部分是状态转移。Roy Fielding 提出一个术语叫将超媒体做为应用状态的引擎(Hypermedia As The Engine Of Application State)。这句话过于拗口,翻译过来更是难以理解,结果被不少人忽略掉了,但这正是 REST 的精髓。

咱们先解释下这个术语。

超媒体:就是咱们再熟悉不过的超连接,HTML 标签中的 a、script、img、link等都属于超媒体连接。

应用状态:这里明确指出是应用的状态而不是资源的。好比上面购物场景中的京东商城就是一个 Web 应用,而应用的状态则是该应用在某时刻呈现出来的样子(各个页面)。

引擎:驱动状态改变(迁移)的东西,说得白话一点就是京东商城的一个个超连接(主要是只 a 标签连接)驱动其从一个页面切换到另外一个页面。

应用为什么要发生状态转移?为了完成一个完整的活动,好比上面的购物。应用本质上是一个有限状态机,其中囊括的一个个活动就是一个个工做流,应用的状态就是工做流中的节点。咱们把上面购物过程画出来以下(只画了主流程,实际中会有不少分支流程,好比用户付款后取消订单、签收后退货等):

李小四购物流程图

这里涉及到一次购物活动(一个大的流程图)中的三个子流程:购物(下单-支付)、查看物流、开发票。每一个节点对应应用的一个状态(也就是页面,前两个是京东商城的,后一个是微信的)。

回想一下李小四是怎样在这些页面(应用状态)间跳来跳去的?不停地在地址栏输入 URL?若是没有超连接(那个小小的 a)恐怕就只能这样了。若是没有超连接,京东首页就不是如今这个样子了,而是一坨长长的 URL 列表,且附上难看的流程图告诉用户要想买一部 iPhone 得按照顺序依次在地址栏输入哪些 URL——这是多么使人崩溃的事情。

因此超连接是个伟大的发明,它使资源(的表述)之间创建联系,用户可以从应用的一个状态转移到另外一个状态,进而完成整个工做流。并且,这种转移是发现式的,即应用的状态切换不是既定的,一个状态的下一个状态可能并不肯定,好比李小四打开京东商城首页后,对某款手表感兴趣,因而点击其连接进入手表详情页——结果买了一款手表而不是 iPhone。

那么,资源的表述应用的状态之间又是什么关系呢?

应用的状态就是资源的表述,或者说应用是经过不一样的资源表述来展示本身的。应用状态的转移就是不一样的资源表述之间或者同一个资源的不一样状态的表述之间的转移。

上面购物流程中,首页是一个特殊的资源;商品列表、商品详情是不一样层次的商品资源;添加购物车生成新的购物车资源(或者更新购物车资源),从建立购物车到购物车详情页属于购物车资源的不一样状态之间的转移;下单操做建立了新的订单资源,支付则产生支付资源,而且在京东商城应用内部产生了一系列新资源好比物流资源;订单签收后开具发票则产生了发票资源。

至此咱们发现,整个 Web 应用的核心仍然是资源,但既不是某一个资源,也不是某几个毫无关联的资源,而是一系列经过超连接创建联系、可以造成工做流来完成一系列活动的有机资源池。

在资源的表述中归入超连接,让资源的表述带有相关资源的 URI,从而让应用可以自动进行状态转移,这种媒体类型(表述)叫超媒体类型。HTML(XHTML) 是最多见的一种超媒体类型,并且是超媒体文本类型(超文本)。虽然 XHTML 基于 XML,但 XML(以及 JSON)不是超媒体类型,它们的原生语义中不带有超连接,没法从 XML 形式的资源表述进入其它资源表述。

XHTML 之因此是超媒体类型,是它在 XML 基础上作了语义化(标记)处理,HTML(XHTML)处理器知道,a 标签表示超连接,点击能够打开新页面,标签表示须要从其指向的 URI 获取图像格式的资源表述,发起 HTTP 请求时会带上诸如 accept: image/*(而不是 text/html)的请求头。

基于 XML 的另外一个普遍使用的超媒体类型是 Atom。

咱们也能够基于 XML 和 JSON 来设计本身的超媒体类型吗?固然能够。好比咱们能够定义以下 JSON 格式:

{
	"id": 123,
	"money": 3000.00,
	...
	"links": [{
		"rel": "mydomain/logistics",
		"uri": "https://www.domain.com/v1/logistics/47589"
	}]
}

其中 links 表示相关资源连接列表,这里给出了本订单相关的物流资源连接。该 JSON 是一个超媒体类型,它不但表述了 123 这个订单资源的信息,还给出了指向相关物流资源的连接。通常地,咱们还要编写对应的 JSON Schema,让其它 JSON 解析器可以理解咱们定义的类型协议。假如咱们将该超媒体类型定义为 application/my.hyperproto+json,可以处理该媒体类型的客户端发起 HTTP 请求时请求头带上 accept:application/my.hyperproto+json,咱们服务器响应时带上 content-type:application/my.hyperproto+json,双方即可以自如地你来我往了(这也正是设计 RESTful API 的一个要点,虽然事实上被大部分实现者忽略了)。

现实:

回顾历史,最先人们并无重视 HTTP 动词和超媒体类型,经过在 URI 中添加动词和类型后缀来表达意图,早期一些浏览器和库甚至不支持除了 GET 和 POST 以外的动词。URI 被动词和类型后缀污染的后果是它再也不是“URI”(资源标识),而是操做者意图传输工具,某些角度说,它影响了 URI 的通用性和可扩展性。

还有一种对 HTTP 协议的退化使用是 XML-RPC,经过一个 URL 搞定一切,其余全部的信息都写在 XML 请求体中——在这里,HTTP 仅仅被当作传输协议而不是应用协议来使用,之
因此使用 HTTP 仅仅是由于它被各类库普遍支持,较好地知足了异构系统环境。

后来,多是一些流行框架的支持,你们赶时髦式地谈论起 RESTful API 起来。这些所谓的 RESTful API 不过是把动词和类型后缀从 URI 中拿走了,给 URI“正了名”,从新用起 HTTP Method。他们并无用起超媒体特性,HTTP 响应类型仅仅是普通的 XML 或 JSON,资源表述自己不能驱动工做流的行进,使用者仍然须要经过带外方式(文档)获取相关资源 URI。

我想,这多是 REST 和 HTTP 协议自身特质形成的。

将操做(动做)极度抽象化(通用化)是一项伟大的设计,但“成也萧何败也萧何”。一方面 HTTP 动词高度抽象化(标准化、通用化),迫使开发者须要绞尽脑汁去把现实世界中成百上千的操做映射到那几个动词上——这不是一项简单的思想活动,同时它还要求开发者需合理的定义“资源”,有些多是极度抽象的。另外一方面,和严谨的动词造成鲜明对比的是 URI(URL)的极度灵活性,开发者能够任意书写 URL,只要能定位到正确的服务器,然后即是“个人地盘我作主”,没有任何硬性约束要求 URL 里面只能出现名词。因而为了少死几个脑细胞,开发人员广泛性地忽略掉 HTTP 动词(甚至忽略掉了媒体类型协商),把这些信息一股脑全塞入那个“万能”的 URL 里面。

使用超媒体的一个困惑是,当咱们使用自定义的超媒体类型时,客户端须要进行额外的解析工做,还不如直接传递你们都认识的 JSON 或 XML 来得短平快。

另外,经过超媒体驱动,意味着应用(系统)仅须要对外公布少数几个入口 URI,其它 URI 都是经过上游资源表述的超连接获取的。那么,咱们到底要暴露哪些入口 URI 呢?这又是一个须要深刻思考的问题,而人都是懒惰的。

不过,REST 给咱们设计 API 提供了一些启示或原则。

  • 在系统的顶层架构上,面向资源而不是操做去规划系统,能站在全局的视角思考系统构架,让系统规划和对外暴露的 API 尽量趋向稳定。
  • URI 仅仅表明资源,经过 HTTP 动词规范化操做,能倒逼咱们更合理地划分资源边界,使得系统更模块化、层次化。另外,它能让咱们更深层次地思考“资源”,好比登陆,好像是个纯动词,但若是进一步思考,登陆这个行为是为了建立会话,对应的登出则是销毁会话,于是咱们操做的其实是“会话”(Session)资源。
  • 尽量使用超媒体类型。经过超连接对外暴露 URI 的一个好处是将具体的 URI 细节隐藏起来,好比上面的 JSON 中,客户端仅关心 rel 的值,而后提取相应的 uri 的值,这里 rel 是不变的,但 uri 可能会发生变化(好比咱们的某个服务外包给第三方了),当 URI变化时,咱们无需广而告之全部的客户端你要改连接哈,不然服务不可用了哈。

总结:

最后咱们总结下对 REST 中资源、表述、状态转移的理解:

  • 资源是服务器端的原始数据,好比订单数据,它是应用的核心。资源经过 URI 对外暴露自身;
  • 在分布式应用中(如 Web),客户端没法直接触达资源自己,能触达的是资源的表述。表述是某种格式的资源副本;
  • 客户端没法经过修改表述(资源副本)来改变资源自己。服务器端拥有资源的控制权,它决定能够提供哪些表述给客户端,也能决定提供什么样的操做(动词);
  • 客户端经过通用动词来获取资源表述以及修改资源状态;
  • 状态是指应用的状态,状态转移体现为应用中工做流程的行进(从一个页面切换到另外一个页面);
  • 状态转移是经过超连接驱动的。超连接由资源的表述携带,这种携带了超连接的表述称为超媒体;
  • 超媒体使得应用可以自我驱动状态转移(而不须要经过带外方式);
相关文章
相关标签/搜索