资源的URL设计
在前面已经提到过,统一接口约束中的第一条子约束就是每个资源都拥有一个资源标识。在正确地辨识出了一个资源之后,我们就需要为这些资源分配其所对应的URI。一个资源所对应的URI可能有多种表示方式,如到底是用单数还是复数表示资源等。因此在一个基于HTTP的REST系统中,如何组织针对各个资源的URL实际上是最重要的一部分。毕竟一个明确的,有意义并且稳定的API接口实际上是对服务对用户的一种承诺。
在HTTP中,一个URL主要由以下几个部分组成:
- 协议。即HTTP以及HTTPS。
- 主机名和端口。如www.egoods.com:8421
- 资源的相对路径。如/api/categories。
- 请求参数。即由问号开始的由键值对组成的字符串:?page=1&page_size=20
在为一个资源设计其所对应的URL时,我们需要着重考虑第三部分和第四部分组成。
通过URL来表示资源
在辨识出了REST系统中的各个资源以后,我们就需要开始为这些资源设计各自所对应的URL了。
首先要介绍的是,所有的资源都应该存在于一个相对路径之下。请读者回忆之前我们介绍的通过向/api发送一个GET请求得到所有可以被访问的资源这个示例:
1 GET /api
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json
5
6 HTTP/1.1 200 OK
7 Content-Type: application/json
8 Content-Length: xxx
9
10 {
11 "version": "1.0",
12 "resources": [
13 {
14 "label" : "Categories",
15 "description" : "Product categories",
16 "uri": "/api/categories"
17 }, {
18 "label" : "Items",
19 "description" : "All items on sell",
20 "uri": "/api/items"
21 }
22 ]
23 }
因此对于从向该相对路径发送请求才能得到的各个主资源来说,将它们置于相对路径/api之下是非常合理的。
除了这个原因之外,API的版本更迭也是一个考虑。假如软件开发人员需要开发一个新版本的REST API,那么他可能就需要重新抽象并定义系统中的各个资源。但是如果两个版本的API中都拥有一个categories资源,并且系统为了保持后向兼容性同时保留了两个版本的API,那么将只有一个资源可以使用/categories这个相对路径。也正因为如此,将这些资源置于相对路径/api之下,并在第二个版本的API出现之后将新的资源抽象置于/api-v2下是一种较为流行的做法。
在明确了所有的资源都应该置于/api这样一个相对路径下之后,我们就来讲解如何为资源定义对应的URL。一个最简单的情况是:指定主资源所对应的URL。由于主资源是一类独立的资源,因此它应该直接置于/api下。例如egoods网站中的产品分类就是一个主资源,我们会为其分配如下URL:
1 /api/categories
而对于其它主资源,如egoods网站中的产品,我们也会为其赋予一个具有类似结构的URL:
1 /api/items
这样,每类主资源都将拥有一个特定于该类资源的URL。这些URL就对应着相应资源实例的集合。
如果需要表示某个主资源类型中的特定实例,那么我们就需要在该类主资源所对应的URL之后添加该实例的ID。如egoods网站中的食品分类的ID为1,那么其所对应的URL就将是:
1 /api/categories/1
一个较为特殊的情况则是,对于某种类型的主资源,整个系统将有且仅有一个该类型资源的实例。那么该资源将不再需要通过ID来访问。我能想到的一个例子就是对整个系统进行介绍的资源。该资源实例所对应的URL将是:
1 /api/about
而一个资源实例中还可能拥有子资源。这些子资源与资源实例之间的关系主要有两种情况:资源实例包含了一个子资源的集合,以及资源实例仅仅可以包含一个子资源。对于资源实例包含了一个子资源集合的情况,我们需要将该子资源集合的URL置于该资源的相对路径下。例如对于egoods上所销售的ID为23456的商品所提供的邮递服务,我们将使用如下的URL:
1 /api/items/23456/shipments
在该URI中,/api/items/23456对应的就是商品本身,而该商品所提供的邮递服务则是该商品的子资源。与主资源特定实例所具有的URI类似,其中一个ID为87256的邮递服务所对应的URI则为:
1 /api/items/23456/shipments/87256
如果资源实例仅仅可以包含一个子资源,那么对该子资源的访问也将不再需要ID。如当前商品的折扣信息:
1 /api/items/23456/discount
单数 vs. 复数
接下来要考虑的一点是,资源在URL中需要由单数表示还是复数表示?这在stackoverflow等众多论坛上已经成为了一个经久不衰的话题。我们知道,在一个基于HTTP的REST系统中,一个资源所对应的URL实际上也就是对其进行操作的URL。因此适当地使用单数和复数对于该系统的用户而言有一定的指示作用。在stackoverflow上的一个常见观点是:如果一个URL所对应的资源是使用复数表示的,那么该类型的资源可能有多个。对该URL发送Get请求可能返回该资源的一个列表。反之,如果一个URL所对应的资源是使用单数表示的,那么该类型的资源将只有一个,因此对该URL发送Get请求将只返回该资源的一个实例。
以egoods中的商品分类为例。由于一个网站所售卖的商品可能有多种类别,因此其需要在URL中使用复数形式:/api/categories。而对于一个该网站的用户而言,由于其只会有一个个人偏好设置,因此其URL则需要使用单数形式:/api/users/{user_id}/preference。
你可能会问:如果需要得到具有特定ID的某个实例时,我们应该对该资源使用单数还是复数呢?答案是复数。这是因为在通过特定ID访问某个资源的实例实际上就是从该资源的集合中取出特定实例。因此表示该资源集合的URL实际上仍然需要使用复数形式,而其后所使用的ID则标明了其所访问的是资源中的单一实例,因此向这个URL发送Get请求将返回该资源的单一实例。
就以“食品”分类为例。该分类所对应的URL为/api/categories/1。该URL中的前半部分/api/categories表示egoods网站中所有分类的集合,而1则表示在该分类集合中的ID为1的分类。
相对路径 vs. 请求参数
另一个经常导致疑惑的地方就是针对资源的某一种特征,我们到底是将其定义为URL中相对路径的一部分还是作为请求参数。
请考虑下面一个例子。在egoods网站中,我们售卖的手机主要有苹果,三星等品牌。那么在为这些手机设计URL的时候,我们是否需要按照品牌对这些手机进行细分,从而用户只要通过向/api/mobiles/brands/apple发送请求就能列出所有的苹果手机?还是说,直接将手机的品牌置于请求参数中,从而通过/api/mobiles?brand=apple来列出所有的苹果手机?
在判断到底是使用请求参数还是相对路径时,我们一般分为下面几步。
首先,可选参数一般都应置于请求参数中。仍以egoods中的手机为例。在选择手机时,用户可以选择品牌以及颜色。如果将品牌和颜色都定义在相对URL中,那么具有特定品牌和颜色的手机将可以通过两个不同的URL访问:/api/mobiles/brand/{brand}/color/{color}以及/api/mobiles/color/{color}/brand/{brand}。就用户而言,其并无法了解这两个URL所表示的是同一类资源还是不同类型的资源。当然,您可以说,我们只用/api/mobiles/brand/{brand}/color/{color}。但是该URL将无法处理用户仅仅选择了颜色,却没有选择品牌的情况。
其次,不是所有字符都可以在URL中被使用,如汉字,标点。为了处理这种情况,包含这些字符的筛选条件需要置于请求参数中。
最后,如果该特征下包含子资源,那么它自身也就是一个资源,因此需要以相对路径的方式展现它。例如在egoods网站中,每件商品所属于的分类仅仅是它的一个特征。但是一个分类更包含了属于它的各个品牌以及热搜关键字等众多信息。因此它其实是一个资源,需要在URI路径中表示它。
总的来说,既然使用HTTP来构建REST系统,那么我们就需要遵守URL各组成中的含义:URL中的相对路径将用来标示“What I want”,也既对应着资源;而请求参数则用来标示“How I want”,即查看资源的方式。