关于 Web 接口的设计,现在网络上聊得最多得是 RESTful. 但很多人仅仅认为 RESTful 是个接口得设计规范,而忽略了它本质上是一个软件设计架构。
在写这篇文章之前,我在网络上查阅了一些文档资料,并从中筛选出我认为写得不错的博客文档。它们是:
阮一峰的两篇博文:
和
来自于就浩这口的四篇博文:
既然已经有这么多的轮子在那里了,我不需要再重复去扯关于 Restful 的各种细节了。所以,我就当下的各种资料,做一些总结性的梳理。是的,也许你不用去读我推荐给你的文章,而直接进入我的总结性的梳理。但你一定得有相关的基础储备,有的是 HTTP 的,有的是 RESTful 的。你的基础储备得有,哪怕是错误的,也是可以的。
当我们将 RESTful 挂在嘴边的时候,可能已经陷入了一个误区。我们忽略了一个事实:HTTP 本身就是 RESTful 的。例如,在 HTTP 里,URI(统一资源定位符)就是用来定位各种资源的。而资源本身可以展示成不同的格式,如 HTML 的、XML 的、JSON 的等等,使用Content-Type
这个响应头来标明响应体的格式。另外,GET、POST、PUT、DELETE 就是来表示对资源的操作的。HTTP 本质上就是 RESTful 的,这是因为,当初在设计 HTTP 的时候,潜意识中就是用的 RESTful 的指导思想。某种程度上说,RESTful 是从 HTTP 的规范中归纳出来的。而不是说,它是后来想出的接口设计规范,用于指导如何设计 HTTP 接口。
所以说,当你使用 HTTP 定义你的接口的时候,它天生就应该是 RESTful. 你不应该去强调你的接口如何 RESTful,而应该说明,你的接口哪些部分没有采纳 RESTful.
至于为什么我们现在看到的接口很多都不是那么的 RESTful 的。一个可能的原因是,HTTP 的设计是个逐步演化的过程。它最初可能就只有 GET、POST 这两个方法,至于其他的方法可能是后来再加上去的。还有一点,人都是懒惰的,一个开发者可能并不想去理解那么多的 HTTP 的概念,而造成了某种误用。
REST,即是 Representational State Transfer 的缩写。它的翻译是”表现层状态转化“。我们就不去理解这个词组的意思了,仅仅需要知道的是,在 REST 世界里,你面向的是资源。
资源,可以理解为一切的实体。这个实体,可以是具体的,例如一本书、一张图片,一首歌曲,一个人。也可以是抽象的,例如一种服务、一个行为。我们需要将转账、关注这些行为都要理解成一种资源。
当我们能够定义和定位资源的时候,针对资源的操作大体只有四种,即 GET、POST、PUT、DELETE,即查看、创建、更新、删除这四种操作。这有点像数据库,面对表的数据只能有增删查改这四种一样。一个局限就是,除了这四种,你不能针对资源有其他的操作,其他的操作必须抽象成另一个资源。这就是面向资源的设计。
资源的操作带来的结果是资源状态的改变。HTTP 是无状态的应用层传输协议,资源的状态只保存在服务器,要想对它做任何改变,只能通过操作改变它。
最后再插入一点。我们用 URI(统一资源定位符)来定位资源,用方法(GET、POST、PUT、DELETE)来表示对资源的操作。另外,我们还会使用大量的头部表示各种元信息。元信息是相当重要的,却是最容易被忽视的部分。在响应体中,还会有一个状态码,顾名思义,它表示这次请求的状态转移的结果。
我不要你觉得,我要我觉得。
在这一篇里,我直接抛出在设计 HTTP 接口时的最佳实践。当然每个人所理解的最佳是不同的,我个人接受反驳,但未必回应。
使用名词定义资源。另外,名词倾向于使用复数。例如用/books
表示一个集合,用/books/:id
表示单个的资源。唯一的使用单数的情况是,它特指某个单个资源。如/system
表示单例资源,/user
特指当前用户。
最简单的是一律只使用 JSON. 当然你也可以同时支持其他的格式,如 XML、formData 等,但务必要清晰。
如果只支持 JSON,那么请求体的Accept
必须注明application/json
,而响应体的Content-Type
也必须是application/json
.
务必使用状态码表示状态,如 4xx 是客户端的错误,5xx 是服务端的错误。但 HTTP 的状态码只是一般性的错误表示,不应该作为开发者处理的字段。我一般会在错误的响应体中定义如下格式:
{
"code": "...",
"message": "...",
"details": {}
}
数据格式都要用一个字段包装起来,无论是请求体还是响应体。这样做是为了保持一致性和方便以后的扩展。
例如单个 book 资源:
{
"book": {
// ...
}
}
和 books 资源集合:
{
"books": [
{ // book 1 },
{ // book 2 }
],
"total": "总数"
}
以上是严格按照 RESTful 所说的而产生的最佳实践的标准。但在我的实际开发中,我往往会做一些妥协。(就像写这篇文章一样,我会随性很多,并做出了很多的妥协)
使用一个特殊的 Header 表示。
我习惯使用一个自定义的X-Token
的头部,使用 jwt 加密。据说 OAuth 2.0 很流行,但我对这一块并不懂。
首先,有些超出”增删查改“范畴的操作并没有抽象成资源,而是作为一个动作附加在资源后面。更确切地说,这一部分没有使用名词,而是作为动词体现在 URI 中。比如,对于关注,我可能是这样设计:
POST /books/:id/star
这里的 star 应理解为动词而不是名词。如果是名词,正如我前面所说的,它应该用复数。
这是我作为 RESTful 的践行者做得不够彻底的地方。
关于 PUT 和 PATCH 的语义,我想在聊聊 RESTful - 接口设计篇(一)里已经解释得很清楚了。在实际的实践中,我往往是把 PUT 作为局部更新来处理的,而 PATCH 和 PUT 一样,并且也不是按照文章说的那样去处理。
关于版本号,我接受在 URI 中加入。这样会更简单。其实,我在实际开发过程中从没有声明过版本号,接口一但有调整都是直接调整,从没有维护过两个版本的情况。这与我只开发公司内部产品,而不是开放 API 有莫大的关系吧。
我们听过面向对象的设计,刚刚也接触过面向资源的接口设计。这里我想提的一点,是面向失败的设计。我们现在所接触的系统,其规模大大超过了以往。我们在上手大部分项目的时候,从一开始,就要考虑到它的完备性,以应对方方面面可能出现的失败。那么,从接口设计开始,我们就要考虑全面一些吧。
我们设想一下客户端调用接口的完整过程,它往往是:
从以上两点看到,服务器需要考虑到一个请求的主要在两个方面:
这分别对应了,在面向失败的设计里,对系统行为预期的最低要求:
一切更优化的最低要求和行为,都是建立在这两点的基础上的。
第一件事情就是要检验请求的数据。首先通用的检验我们就不展开说了,如你的数据是合法的 JSON 格式,这应该是在一开始就限制好的。
针对一个合法的 JSON 数据本身,通常需要考虑的问题是:
第二件事情就是验证操作权限,以防止没有权限的用户对它管辖外的资源造成的破坏。鉴权系统首先需要验证客户端是否提供了有效的身份信息,可选的方案如 jwt. 然后,它需要在两个维度下验证权限:
在安全的接口设计里,我们需要考虑哪些数据是允许暴露给客户端的。这也包括两个维度:
这一节提到了三点常见的失败可能。失败的可能错综复杂,将这三点单独拎出来说的原因是,它们是最常见的。所以,在框架选型和项目构建中,一开始就要将这三点囊括进来。现代的 WEB 框架往往能够采用一套通用的方式处理,包括用户代码和测试代码,算是为我接下来讲框架的内容埋下铺垫。