目录
API设计的许多讨论都是从如何设计URL开始的。在像REST这样的面向数据的模型中,我们认为最好从表示形式(Representations)设计开始。 我们将在“Designing URL”一节中讨论URL设计。
表现形式(representation)作为技术术语,其含义可描述为:
客户端从服务器检索Web资源或从客户端发送到服务器时返回的数据
在REST模型中,Web资源不具有直观的基础状态,「在客户端和服务器之间所流动着的」,便是对那种状态的一种描述。用户可以查看一个资源不同形式的"表现形式"(representation),而这被称为“MEDIA-TYPE”。原则上,用于表示特定资源的所有媒体类型都可以以不同的格式来编码相同的信息。
使用JSON
Web API中资源表示的主要媒体类型是JSON(JavaScript Object Notation)。 JSON成功的主要原因可能是易于理解,并且易于映射到JavaScript和其他流行编程语言(Python,Ruby,Java等)的数据结构中。现在,您应该使用JSON,因为它已成为Web API事实上的标准。
虽然JSON很好而且很受欢迎,但对于我们的目的而言并不是完美的。一个局限性是JSON只能表示少量的数据类型(Null,Boolean,Number,String)。在Web_API设计中,JSON不支持的最常见类型是日期,时间和URL。有几种解决此限制的方法-最简单的方法是将它们表示为字符串,然后根据上下文确定哪些字符串只是字符串,哪些字符串是真正的日期或URL。 我们也将在本书中进一步讨论其他选项。
让JSON简洁化
当把JSON用好时,它是简单、直观且在很大程度上一目了然的。如果您的JSON看起来不像下面的示例那么简单,则可能是您做错了。
{
“kind”: “Dog”,
“name”: “Lassie”,
“furColor”: “brown”,
...
}
JSON规范仅说明JSON对象是“key/_value”对的集合,除了将其约束为字符串外,它没有说明名称是什么。在此示例中名称对应于—Web资源的属性,其表示形式是封闭的JSON对象。这些属性名称有时称为谓词,这是从语法理论中借用的术语。 如果您遵循
JSON中的名称始终为合适的名称,并且JSON对象始终与API数据模型中的实体相对应
的原则,那么JSON将变得更容易理解。
以下是Facebook graph API的示例,该示例显示了不遵循此建议的JSON的样子:
{
“{
user-id-a
}”: {
“data”: [
{
“id”: “12345”,
“picture”: “{
photo-url
}”,
“created_time”: “2014-07-15T15: 11: 25+0000”
}...//Morephotos
]
},
“{
user-id-b
}”: {
“data”: [
{
}……
请注意,在其他的JSON实例中,冒号左侧都有着合适的名称,然而观察user-ids时明显地不合法。同时JSON中的名称"data"也没有与数据模型合适地对应上。我们认为,这样的JSON比我们的第一个示例以及Google和GitHub的示例中的简单JSON更加难以学习和理解。 我们建议您坚持简单的做法。
包含链接
除简单属性外,大多数问题域还包括概念关系。 表示HTTP中关系的自然结构是链接。
在过去的几年中,API中链接的使用与以下想法紧密相关:API客户端可以并且应该被编写为像Web浏览器一样工作-这意味着它们对任何特定API的数据或语义以及它们的先验知识都不了解。行为完全由服务器返回的数据内容驱动,尤其是链接(备注:3)。这个想法被称为“Hypermedia As The Engine Of Application State” 或者“HATEOAS(备注:4)”。Web浏览器以这种方式工作是有意义的,因为为Web上的每个站点编写自定义Web浏览器是不切实际的。这样工作的客户很多比起具有特定API内置知识的客户端,编写起来更加困难和昂贵。除了构建成本更高之外,通用API客户端通常也不太令人满意,尤其是具有用户接口的客户端,因此很少构建此类API客户端,这种方法被大多数人认为不切实际。近年来的一个变化是,人们意识到使用链接可以大大改善所有API的可用性和可学习性,而不仅仅是那些旨在由完全通用的客户端使用的API。
再次使用前言中介绍的示例,假设狗与狗的主人之间存在某种关系。在JSON中表示关系信息的一种流行方式如下所示:
{
“id”: “12345678”,
“name”: “Lassie”,
“furColor”: “brown”,
“ownerID”: “98765432”
}
ownerID字段表示关系。 表达关系的一种更好的方法是使用链接。 如果您的Web—API今天不包含链接,那么第一步就是简单地添加一些链接而不进行其他更改,例如:
{
“id”: “12345678”,
“kind”: “Dog”“name”: “Lassie”,
“furColor”: “brown”,
“ownerID”: “98765432”,
ownerLink”: “https: //dogtracker.com/persons/98765432”
}
如果对于owner以前看起来像这样表示:
{
“id”: ”98765432”,
“kind”: “Person”“name”: “JoeCarraclough”,
“hairColor”: “brown”
}
你可以把它完善成这样:
{
“id”: ”98765432”,
“kind”: “Person”“name”: “JoeCarraclough”,
“hairColor”: “brown”,
“dogsLink”: “https: //dogtracker.com/persons/98765432/dogs”
}
为什么这样更好?
在原始示例中,如果您具有Lassie(示例中的狗)的表示形式,并且想要获取Lassie的所有者Joe的表示形式,则必须查看API的文档以找到可用于构造URL的合适的URI模板来找到Joe。在Lassie的表示中,您拥有一个ownerID,并且您正在寻找一个以ownerID为变量并生成查找所有者的URL。这种方法有几个缺点:
1)一种是它要求您去寻找文档,而不仅仅是查看数据。
2)另一个是,实际上没有很好的文档约定来描述可以将表示形式中的哪些属性值插入哪些模板中,因此即使给出了正确的文档,通常也需要进行一些猜测。在此示例中,您将必须知道所有者是人,并查找将人的ID作为变量的模板。
3)第三个缺点是您必须编写代码将ownerID属性值与模板结合在一起,并生成一个URL。 URI模板规范将此类代码称为模板处理器。对于简单的模板,此代码相当简单,但仍必须编写和调试。
相比之下,在我们经过修改的设计中,需要学习的东西更少,并且使用API更容易。 Joe的URL就在数据中,您可以查看,因此您无需深入研究文档。您的客户端代码也更容易编写-只需选择ownerLink的值并在HTTP请求中使用它即可。您甚至可以编写和使用遵循这些链接的通用代码,而无需了解API的任何内在知识。将dogsLink添加到所有者资源可提供更多价值。 如果没有dogsLink,从owner导航到dog将会很不容易。网络上越来越多的大型公司的API都在其数据中包含了链接。我们将在“谁使用链接”中分享一些Google和GitHub的示例。
当有链接时URI模板还有用吗?
是的,它们仍然很重要。 链接阐释了"你从哪里来,你要到哪里去",链接就像是“铺着青砖小路旁的路标”,而URI模版则像是“对明确目标的快捷方式”,在具有链接的API中,经常发生的是,资源中已有一个方便的链接,您已经拥有该链接的表示形式,可以将您带到您想去的地方。也可能发生的是访问资源时没有链接,或者通过链接到达目标的路径过长。在这种情况下,您需要另一种方法来定位需要获取的资源。 URI模板提供了一种方法。
如果URI模板接受变量的值易于人类阅读和记忆,则它们是最有用的。 例如:
https://dogtracker.com/persons/{personID}
如上的URL有一些效用,但是如果personID比较朦胧或者冗余,则记住personID几乎和记住整个URL一样困难。相反,参考如下模版:
https://dogtracker.com/persons/{personName}
personName则更有用,因为名称对于人类与世界打交道的方式至关重要。
这与域名映射到IP地址的原理相同。例如,在网络上,当您跟踪到运行Google Cloud博客的服务器的链接时,无需关心该链接是包含域名还是IP地址,而只需使用它即可。输入域名blog.apgiee.com确实要比输入URL体验更好。
类比万维网
HTML Web是通过HTML链接连接的文档网络。浏览网络的一种基本方法是跟随链接从一个资源跳到另一个资源。 您也可以直接输入URL或从书签访问URL,但是您可能记住或添加书签的URL数量是有限的。 在最初的互联网时代,只有链接或输入URL是唯一的导航方法。一些公司(最著名的Yahoo!)扩展了链接遍历机制,以解决资源发现的问题。 比如“https://yahoo.com”将成为Web访问的根路径,从那里您可以浏览Yahoo类别中的链接以到达所需的任何位置。其他公司(最著名的公司是Google)可以让您使用网页上已知的信息(出现在网页文字中的关键字或字符串)执行网页搜索。 发现了这些页面后,您可以单击它们包含的链接以进入新页面,直到用尽有用的链接以供点击,然后必须返回Google进行其他搜索。现在,我们所有人都理所当然地认为,通过搜索和链接遍历相结合是使用Web的普遍方式。
典型Web API的URI模板的一种方法是类似于Google搜索。这使得您可以通过您已经知道的一些信息来查找您不知道其URL的资源。大多数人都知道如何使用此类网址通过网络浏览器执行Google搜索:
https://www.google.com/search?q=web+api+design
当web API设计者定义类似如下URL模板时:
https://dogtracker.com/persons/{personId}/dogs
他们实际上是为其应用程序指定一种特殊的查询语言。 相同的查询可以表示为:
https://dogtracker.com/search?type=Dog&ownerId={personId}
与Google在所有网站上提供通用搜索语言不同,URI模板定义了一种特定于特定资源集-API的查询语言。 设计特定于您的API的查询语言可能会有所帮助,因为您可以利用对数据和用例的了解来为您的资源定义更有用和更易理解的查询功能,并限制实现它的成本。缺点是必须分别学习每种API的查询语言。 如果存在所有API支持的通用查询语言,那么所有API客户端应用程序开发人员的工作效率都将得到提高。我们将在标题为“Designing_query_URL”的部分中介绍如何使查询URL规则。
许多人对URI模板通过查询定位资源的成功感到满意,而忽略了其他机制(链接)的价值。另一方面,一些链接倡导者坚持不使用查询或搜索的单一根模型。就像HTMLWeb一样,这两种技术是对Web API彼此有价值的补充。
包含链接,步骤二
如果您要做的就是添加上面显示的URL值属性,那么您已经对API进行了重大改进。 您可以将URL值属性设置为只读,并继续以今天的方式进行更新。 例如,如果您想将Lassie的所有者更改为Duke,则可以使用PUT或PATCH请求修改Lassie,以将ownerID的值更改为Duke的ID,服务器将重新计算ownerLink属性。如果要开发现有的API,这是一个非常实际的步骤。但是,如果您正在设计一个新的API,则可能希望通过链接更进一步。
为了激励如上提到的“通过链接更进一步”,请考虑以下示例。假设狗可以归个人或机构所有(例如公司,教堂,政府,慈善机构和非政府组织)。 我们的“dog tracker”站点致力于启用此模型,以便它们可以跟踪警犬和其他不属于个人的工作犬的所有权。还假定机构与狗存储在不同的表(或数据库)中,它们具有自己的ID集。 在这一点上,拥有一个具有简单值来引用所有者的ownerID属性已经远远不够—您还需要指定所有者类型,以便服务器知道要查找的表(或数据库)。 可能的解决方案是:您可以具有ownerID和ownerType属性,或者可以具有单独的personOwnerID和stitutionalOwnerID属性,一次只能设置其中之一。您还可以发明一个对类型和ID进行编码的复合所有者值。 这些解决方案中的每一个都有其优缺点,但是一个非常优雅和灵活的选择是使用链接来解决问题。 想象一下,我们将示例修改为如下所示:
{
“self”: “https: //dogtracker.com/dogs/12345678”,
“id”: “12345678”,
“kind”: “Dog”“name”: “Lassie”,
“furColor”: “brown”,
“owner”: “https: //dogtracker.com/persons/98765432”
}
显式的变化是删除了ownerID属性,并且对于owner字段,现在只有一个URL值的字段。隐含的更改是,URL值的owner属性现在是可读写的,而不仅仅是只读的。现在,您只需将属性设置为人的URL或机构的URL,即可轻松容纳人类和机构所有者。客户必须意识到所有者URL可能指向不同种类的资源,并且他们必须准备根据导航此链接时返回的数据做出不同的反应。请注意,这恰恰是网络浏览器的行为-浏览器将跟随网页中的链接,然后在另一端(HTML页面,XML,PDF,JPEG等)根据发现的内容采取适当的措施。我们建议使用这种功能强大且灵活的模型,即使它对某些客户有一些不习惯的要求也是如此。
注意事项
我们非常喜欢具有URL值读写属性的模型,但这对服务器实现者有影响。通常,在数据库中存储带有域名的完整URL不是一个好主意。如果系统域名发生更改,则数据库中的所有这些URL都将是错误的。有时不可避免地要更改生产服务器的域名,即使这是不希望的。即使设法避免生产服务器名称更改,您也希望可以自由使用具有不同域名或IP地址的不同环境(例如集成测试,系统测试和预生产)中的同一数据库。这意味着服务器应在存储资源之前从标识其自身资源的URL中剥离方案和权限,并在请求时将其放回原处。「标识完全在您的API外部的资源的任何URL,可能都必须存储为绝对URL。(Any URLs that identify resources that are completely external to your API will probably have to be stored as absolute URLs.)」如果您对数据库中的绝对URL不太熟悉,请不要这样做。
避免数据库中绝对URL问题的一种方法是在API中接受并生成相对URL,尤其是那些以单个/开头的规范中称为“绝对路径URL”的URL。这是一种合理的方法,它可以避免服务器上的许多实现问题。 折衷方案是给客户端带来一些负担,因为客户端在使用它们之前必须将这些相对URL转换为绝对URL。 对客户来说,好消息是,总是有可能仅使用URL标准的常识就可以将相对URL转换为绝对URL,而无需任何需要特定于您API的其他知识。坏消息是,在某些情况下,客户端使用“相对URL”可能很棘手。如果您愿意学习如何在服务器上产生和使用绝对URL,则可以为用户创建更好的API。
在资源中如何表示链接?
我们表示链接的首选方法是使用简单的JSON名称/值对,如下所示:
“owner”: “https://dogtracker.com/persons/98765432”
这种方法有什么优势? 它非常简单,并且与我们在JSON中表示简单属性的方法是一致的。缺点是JSON没有URL值的编码,因此您必须将它们编码为字符串。 如果客户没有元数据,他们不得不猜测哪些字符串实际上是URL,这可能需要通过解析所有字符串值或依靠上下文信息来进行。 对于自定义编写的客户端代码来说,这通常不是问题,因为通常它具有上下文信息,这是由程序员编写的,该程序员查看了文档并提前知道了哪些属性具有URL值。当您在编写通用客户端或库代码时,若此时不知道其API表现形式的具体细节时,就会出现问题。
有许多用于表示链接的更复杂的模式。 附录中包含了更多有关这些的信息。 无论您是否喜欢或是否使用其中任何一种,我们都认为必须要注意,使用链接并不需要更复杂的东西,简单的JSON属性就够用了。我们已经完成了一些使用不同样式的项目,并且我们目前倾向于使事情尽可能简单。
谁使用链接?
上面关于在API中包含链接的建议并不是已发布的API中最常见的做法,但是他们被用在一些非常著名的地方,例如Google Drive API和GitHub。 以下是来自Google Drive API和GitHub的示例。 您可能会认为Google Drive的问题域使其特别适合这些技术,但我们认为它们同样适用于更广泛的问题域,并且我们在许多自己的设计中都使用了它们。
这是Google Drive API示例:
GET
https://www.googleapis.com/drive/v2/files/0B8G-Akr_SmtmaEJneEZLYjB HTTP/1.1 200 OK
…
{
kind”: “drive#file”,
“id”: “0B8G-Akr_SmtmaEJneEZLYjBBdWxxxxxxxxxxxxxxxx”,
“etag”: “\”btSRMRFBFi3NMGgScYWZpc9YNCI/MTQzNjU2NDk3OTU3Nw\””,
“selfLink”: “https: //www.googleapis.com/drive/v2/files/0B8G-Akr_Smtm“webContentLink”: “https: //docs.google.com/uc?id=0B8G-Akr_SmtmaEJneE“alternateLink”: “https: //drive.google.com/file/d/0B8G-Akr_SmtmaEJne“iconLink”: “https: //ssl.gstatic.com/docs/doclist/images/icon_12_pdf“thumbnailLink”: “https: //lh4.googleusercontent.com/REcVMLRuNGsohM1C“title”: “SoilsReport.pdf”,
“mimeType”: “application/pdf”,
…“parents”: [
{
“kind”: “drive#parentReference”,
“id”: “0AMG-Akr_SmtmUk9PVA”,
“selfLink”: “https: //www.googleapis.com/drive/v2/files/0B8G-Akr_Sm“parentLink”: “https: //www.googleapis.com/drive/v2/files/0AMG-Akr_“isRoot”: false
}
],
…
(备注:此示例中的行相当长,在此处右侧被截断。)
这是来源于GitHub API的另外一个不同的示例:
{
“id”: 1,
“url”: “https: //api.github.com/repos/octocat/Hello-World/issues/1347”,
“repository_url”: “https: //api.github.com/repos/octocat/Hello-World”,
“Labels_url”: ”https: //api.github.com/repos/octocat/Hello-World/issues/1347/labels{
/name
}”,
“comments_url”: “https: //api.github.com/repos/octocat/Hello-World/issues/1347/comments”,
“events_url”: “https: //api.github.com/repos/octocat/Hello-World/issues/1347/events”,
“html_url”: “https: //github.com/octocat/Hello-World/issues/1347”,
“number”: 1347,
“state”: “open”,
“title”: “Foundabug”,
“body”: “I’mhavingaproblemwiththis.”,
“user”: {
“login”: “octocat”,
“id”: 1,
“avatar_url”: “https: //github.com/images/error/octocat_happy.gif”,
“gravatar_id”: “”,
“url”: “https: //api.github.com/users/octocat”,
“html_url”: “https: //github.com/octocat”,
“followers_url”: “https: //api.github.com/users/octocat/followers”,
“following_url”: “https: //api.github.com/users/octocat/following{
/other_user
}”,
“gists_url”: “https: //api.github.com/users/octocat/gists{
/gist_id
}”,
“starred_url”: “https: //api.github.com/users/octocat/starred{
/owner
}{
/repo
}”,
“subscriptions_url”: “https: //api.github.com/users/octocat/subscriptions”,
“organizations_url”: “https: //api.github.com/users/octocat/orgs”,
“repos_url”: “https: //api.github.com/users/octocat/repos”,
“events_url”: “https: //api.github.com/users/octocat/events{
/privacy
}”,
“received_events_url”: “https: //api.github.com/users/octocat/received_events”,
“type”: “User”,
“site_admin”: false
},
“labels”: [
{
“url”: “https: //api.github.com/repos/octocat/Hello-World/labels/bug”,
“name”: “bug”,
“color”: “f29513”
}
],
您会注意到GitHub示例中并非全部都是简单URL。其中一些是URI模板。模板允许整个资源族的URL以一个字符串进行通信,但需要客户端处理模板。
更多
关于设计表示形式(representation),我们将在标题为“More on representation design"的部分中继续讨论。现在,我们将注意力转向URL设计。