001-web api design-概述简介
Posted 木子旭
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了001-web api design-概述简介相关的知识,希望对你有一定的参考价值。
一、概述
因为REST是一种架构风格而不是严格的标准,所以它可以灵活地实现。由于这种灵活性和结构自由度,对设计最佳实践也有很大的差异。
API的方向是从应用程序开发人员的角度考虑设计选择。
幂等性
不要从字面意思来理解什么是幂等性,恰恰相反,这与某些功能紊乱的领域无关。下面是来自维基百科的解释:
在计算机科学中,术语幂等用于更全面地描述一个操作,一次或多次执行该操作产生的结果是一致的。根据应用的上下文,这可能有不同的含义。例如,在方法或者子例程调用具有副作用的情况下,意味着在第一调用之后被修改的状态也保持不变。
从REST服务端的角度来看,由于操作(或服务端调用)是幂等的,客户端可以用重复的调用而产生相同的结果——在编程语言中操作像是一个"setter"(设置)方法。换句话说,就是使用多个相同的请求与使用单个请求效果相同。注意,当幂等操作在服务器上产生相同的结果(副作用),响应本身可能是不同的(例如在多个请求之间,资源的状态可能会改变)。
PUT和DELETE方法被定义为是幂等的。查看http请求中delete动词的警告信息,可以参照下文的DELETE部分。GET、HEAD、OPTIO和TRACE方法自从被定义为安全的方法后,也被定义为幂等的。参照下面关于安全的段落。
安全
来自维基百科:
一些方法(例如GET、HEAD、OPTIONS和TRACE)被定义为安全的方法,这意味着它们仅被用于信息检索,而不能更改服务器的状态。换句话说,它们不会有副作用,除了相对来说无害的影响如日志、缓存、横幅广告或计数服务等。任意的GET请求,不考虑应用状态的上下文,都被认为是安全的。
总之,安全意味着调用的方法不会引起副作用。因此,客户端可以反复使用安全的请求而不用担心对服务端产生任何副作用。这意味着服务端必须遵守GET、HEAD、OPTIONS和TRACE操作的安全定义。否则,除了对消费端产生混淆外,它还会导致Web缓存,搜索引擎以及其它自动代理的问题——这将在服务器上产生意想不到的后果。
根据定义,安全操作是幂等的,因为它们在服务器上产生相同的结果。
安全的方法被实现为只读操作。然而,安全并不意味着服务器必须每次都返回相同的响应。
1.1、RestFul准则
REST架构方式描述了六种设计准则。这些用于架构的设计准则,最早是由Roy Fielding在他的博士论文中提出并定义了RESTful风格。(详见http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)
六个设计准则分别是:统一接口、无状态、可缓冲、C-S架构、分层系统、按需编码
1、统一接口
统一接口准则定义了客户端和服务端之间的接口,简化和分离了框架结构,这样一来每个部分都可独立演化。以下是接口统一的四个原则:
基于资源
不同资源需要用URI来唯一标识。返回给客户端的表征和资源本身在概念上有所不同,例如服务端不会直接传送一个数据库资源,然而,一些html、XML或JSON数据能够展示部分数据库记录,如用芬兰语来表述还是用UTF-8编码则要根据请求和服务器实现的细节来决定。
通过表征来操作资源
当客户端收到包含元数据的资源的表征时,在有权限的情况下,客户端已掌握的足够的信息,可以对服务端的资源进行删改。
自描述的信息
每条信息都包含足够的数据用以确认信息该如何处理。例如要由网络媒体类型(已知的如MIME类型)来确认需调用哪个解析器。响应同样也表明了它们的缓存能力。
超媒体即应用状态引擎(HATEOAS)
客户端通过body内容、查询串参数、请求头和URI(资源名称)来传送状态。服务端通过body内容,响应码和响应头传送状态给客户端。这项技术被称为超媒体(或超文本链接)。
除了上述内容外,HATEOS也意味着,必要的时候链接也可被包含在返回的body(或头部)中,以提供URI来检索对象本身或关联对象。下文将对此进行更详细的阐述。
统一接口是每个REST服务设计时的必要准则。
2、无状态
正如REST是REpresentational State Transfer的缩写,无状态很关键。本质上,这表明了处理请求所需的状态已经包含在请求本身里,也有可能是URI的一部分、查询串参数、body或头部。URI能够唯一标识每个资源,body中也包含了资源的转态(或转态变更情况)。之后,服务器将进行处理,将相关的状态或资源通过头部、状态和响应body传递给客户端。
从事我们这一行业的大多数人都习惯使用容器来编程,容器中有一个“会话”的概念,用于在多个HTTP请求下保持状态。在REST中,如果要在多个请求下保持用户状态,客户端必须囊括客户端的所有信息来完成请求,必要时重新发送请求。自从服务端不需要维持、更新或传递会话状态后,无状态性得到了更大的延展。此外,负载均衡器无需担心和无状态系统之间的会话。
所以状态和资源间有什么差别?服务器对于状态,或者说是应用状态,所关注的点是在当前会话或请求中要完成请求所需的数据。而资源,或者说是资源状态,则是定义了资源表征的数据,例如存储在数据库中的数据。由此可见,应用状态是是随着客户端和请求的改变而改变的数据。相反,资源状态对于发出请求的客户端来说是不变的。
在网络应用的某一特定位置上摆放一个返回按钮,是因为它希望你能按一定的顺序来操作吗?其实是因为它违反了无状态的原则。有许多不遵守无状态原则的案例,例如3-Legged OAuth,API调用速度限制等。但还是要尽量确保服务器中不需要在多个请求下保持应用状态。
3、可缓存
在万维网上,客户端可以缓存页面的响应内容。因此响应都应隐式或显式的定义为可缓存的,若不可缓存则要避免客户端在多次请求后用旧数据或脏数据来响应。管理得当的缓存会部分地或完全地除去客户端和服务端之间的交互,进一步改善性能和延展性。
4、C-S架构
统一接口使得客户端和服务端相互分离。关注分离意味什么?打个比方,客户端不需要存储数据,数据都留在服务端内部,这样使得客户端代码的可移植性得到了提升;而服务端不需要考虑用户接口和用户状态,这样一来服务端将更加简单易拓展。只要接口不改变,服务端和客户端可以单独地进行研发和替换。
5、分层系统
客户端通常无法表明自己是直接还是间接与端服务器进行连接。中介服务器可以通过启用负载均衡或提供共享缓存来提升系统的延展性。分层时同样要考虑安全策略。
6、按需编码(可选)
服务端通过传输可执行逻辑给客户端,从而为其临时拓展和定制功能。相关的例子有编译组件Java applets和客户端脚本javascript。
遵从上述原则,与REST架构风格保持一致,能让各种分布式超媒体系统拥有期望的自然属性,比如高性能,延展性,简洁,可变性,可视化,可移植性和可靠性。
提示:REST架构中的设计准则中,只有按需编码为可选项。如果某个服务违反了其他任意一项准则,严格意思上不能称之为RESTful风格。
二、HTTP动词表示含义
任何API的使用者能够发送GET、POST、PUT和DELETE请求,它们很大程度明确了所给请求的目的。同时,GET请求不能改变任何潜在的资源数据。测量和跟踪仍可能发生,但只会更新数据而不会更新由URI标识的资源数据。
Http动词主要遵循“统一接口”规则,并提供给我们对应的基于名词的资源的动作。最主要或者最常用的http动词(或者称之为方法,这样称呼可能更恰当些)有POST、GET、PUT和DELETE。这些分别对应于创建、读取、更新和删除(CRUD)操作。也有许多其它的动词,但是使用频率比较低。在这些使用较少的方法中,OPTIONS和HEAD往往使用得更多。
1、GET【检索】
HTTP的GET方法用于检索(或读取)资源的数据。在正确的请求路径下,GET方法会返回一个xml或者json格式的数据,以及一个200的HTTP响应代码(表示正确返回结果)。在错误情况下,它通常返回404(不存在)或400(错误的请求)。
例如:
GET http://www.example.com/customers/12345
GET http://www.example.com/customers/12345/orders
GET http://www.example.com/buckets/sample
按照HTTP的设计规范,GET(以及附带的HEAD)请求仅用于读取数据而不改变数据。因此,这种使用方式被认为是安全的。也就是说,它们的调用没有数据修改或污染的风险——调用1次和调用10次或者没有被调用的效果一样。此外,GET(以及HEAD)是幂等的,这意味着使用多个相同的请求与使用单个的请求最终都拥有相同的结果。
不要通过GET暴露不安全的操作——它应该永远都不能修改服务器上的任何资源。
2、PUT【更新】
PUT通常被用于更新资源。通过PUT请求一个已知的资源URI时,需要在请求的body中包含对原始资源的更新数据。
不过,在资源ID是由客服端而非服务端提供的情况下,PUT同样可以被用来创建资源。换句话说,如果PUT请求的URI中包含的资源ID值在服务器上不存在,则用于创建资源。同时请求的body中必须包含要创建的资源的数据。有人觉得这会产生歧义,所以除非真的需要,使用这种方法来创建资源应该被慎用。
或者我们也可以在body中提供由客户端定义的资源ID然后使用POST来创建新的资源——假设请求的URI中不包含要创建的资源ID(参见下面POST的部分)。
例如:
PUT http://www.example.com/customers/12345
PUT http://www.example.com/customers/12345/orders/98765
PUT http://www.example.com/buckets/secret_stuff
当使用PUT操作更新成功时,会返回200(或者返回204,表示返回的body中不包含任何内容)。如果使用PUT请求创建资源,成功返回的HTTP状态码是201。响应的body是可选的——如果提供的话将会消耗更多的带宽。在创建资源时没有必要通过头部的位置返回链接,因为客户端已经设置了资源ID。请参见下面的返回值部分。
PUT不是一个安全的操作,因为它会修改(或创建)服务器上的状态,但它是幂等的。换句话说,如果你使用PUT创建或者更新资源,然后重复调用,资源仍然存在并且状态不会发生变化。
例如,如果在资源增量计数器中调用PUT,那么这个调用方法就不再是幂等的。这种情况有时候会发生,且可能足以证明它是非幂等性的。不过,建议保持PUT请求的幂等性。并强烈建议非幂等性的请求使用POST。
3、POST【创建】
POST请求经常被用于创建新的资源,特别是被用来创建从属资源。从属资源即归属于其它资源(如父资源)的资源。换句话说,当创建一个新资源时,POST请求发送给父资源,服务端负责将新资源与父资源进行关联,并分配一个ID(新资源的URI),等等。
例如:
POST http://www.example.com/customers
POST http://www.example.com/customers/12345/orders
当创建成功时,返回HTTP状态码201,并附带一个位置头信息,其中带有指向最先创建的资源的链接。
POST请求既不是安全的又不是幂等的,因此它被定义为非幂等性资源请求。使用两个相同的POST请求很可能会导致创建两个包含相同信息的资源。
PUT和POST的创建比较
总之,我们建议使用POST来创建资源。当由客户端来决定新资源具有哪些URI(通过资源名称或ID)时,使用PUT:即如果客户端知道URI(或资源ID)是什么,则对该URI使用PUT请求。否则,当由服务器或服务端来决定创建的资源的URI时则使用POST请求。换句话说,当客户端在创建之前不知道(或无法知道)结果的URI时,使用POST请求来创建新的资源。
4、DELETE【删除】
DELETE很容易理解。它被用来根据URI标识删除资源。
例如:
DELETE http://www.example.com/customers/12345
DELETE http://www.example.com/customers/12345/orders
DELETE http://www.example.com/buckets/sample
当删除成功时,返回HTTP状态码200(表示正确),同时会附带一个响应体body,body中可能包含了删除项的数据(这会占用一些网络带宽),或者封装的响应(参见下面的返回值)。也可以返回HTTP状态码204(表示无内容)表示没有响应体。总之,可以返回状态码204表示没有响应体,或者返回状态码200同时附带JSON风格的响应体。
根据HTTP规范,DELETE操作是幂等的。如果你对一个资源进行DELETE操作,资源就被移除了。在资源上反复调用DELETE最终导致的结果都相同:即资源被移除了。但如果将DELETE的操作用于计数器(资源内部),则DETELE将不再是幂等的。如前面所述,只要数据没有被更新,统计和测量的用法依然可被认为是幂等的。建议非幂等性的资源请求使用POST操作。
然而,这里有一个关于DELETE幂等性的警告。在一个资源上第二次调用DELETE往往会返回404(未找到),因为该资源已经被移除了,所以找不到了。这使得DELETE操作不再是幂等的。如果资源是从数据库中删除而不是被简单地标记为删除,这种情况需要适当妥协。
下表总结出了主要HTTP的方法和资源URI,以及推荐的返回值:
HTTP请求 | /customers | /customers/{id} |
GET | 200(正确),用户列表。使用分页、排序和过滤大导航列表。 | 200(正确),查找单个用户。如果ID没有找到或ID无效则返回404(未找到)。 |
PUT | 404(未找到),除非你想在整个集合中更新/替换每个资源。 | 200(正确)或204(无内容)。如果没有找到ID或ID无效则返回404(未找到)。 |
POST | 201(创建),带有链接到/customers/{id}的位置头信息,包含新的ID。 | 404(未找到) |
DELETE | 404(未找到),除非你想删除整个集合——通常不被允许。 | 200(正确)。如果没有找到ID或ID无效则返回404(未找到)。 |
三、合理的资源命名
URL命名:名词优先,禁止动词
RESTful设计中的首要原则是:简单易懂。
1、保持您的基本网址简单直观
2、将动词保留在基本URL之外,URL是自解释的
3、使用HTTP谓词对集合和元素进行操作。
合理的资源名称或者路径(如/posts/23而不是/api?type=posts&id=23)可以更明确一个请求的目的。使用URL查询串来过滤数据是很好的方式,但不应该用于定位资源名称。
适当的资源名称为服务端请求提供上下文,增加服务端API的可理解性。通过URI名称分层地查看资源,可以给使用者提供一个友好的、容易理解的资源层次,以在他们的应用程序上应用。资源名称应该是名词,避免为动词。使用HTTP方法来指定请求的动作部分,能让事情更加的清晰。
从本质上讲,一个RESTFul API最终都可以被简单地看作是一堆URI的集合,HTTP调用这些URI以及一些用JSON和(或)XML表示的资源,它们中有许多包含了相互关联的链接。RESTful的可寻址能力主要依靠URI。每个资源都有自己的地址或URI——服务器能提供的每一个有用的信息都可以作为资源来公开。统一接口的原则部分地通过URI和HTTP动词的组合来解决,并符合使用标准和约定。
在决定你系统中要使用的资源时,使用名词来命名这些资源,而不是用动词或动作来命名。换句话说,一个RESTful URI应该关联到一个具体的资源,而不是关联到一个动作。另外,名词还具有一些动词没有的属性,这也是另一个显著的因素。
服务套件中的每个资源至少有一个URI来标识。如果这个URI能表示一定的含义并且能够充分描述它所代表的资源,那么它就是一个最好的命名。URI应该具备可预测性和分层结构,这将有助于提高它们的可理解性和可用性的:可预测指的是资源应该和名称保持一致;而分层指的是数据具有关系上的结构。这并非REST规则或规范,但是它强化了对API的定义。
RESTful API是提供给消费端的。URI的名称和结构应该将它所表达的含义传达给消费者。通常我们很难知道数据的边界是什么,但是从你的数据上你应该很有可能去尝试找到要返回给客户端的数据是什么。API是为客户端而设计的,而不是为你的数据。
2、示例
示例一、基础示例:
对产品相关的URI的一些建议: POST http://www.example.com/products 用于创建新的产品。 GET|PUT|DELETE http://www.example.com/products/66432 分别用于读取、更新、删除编号为66432的产品。
示例二、为用户创建一个新的订单
一种方案是: POST http://www.example.com/orders 这种方式可以用来创建订单,但缺少相应的用户数据。 方案二、因为我们想为用户创建一个订单(注意之间的关系),这个URI可能不够直观,下面这个URI则更清晰一些: POST http://www.example.com/customers/33245/orders 现在我们知道它是为编号33245的用户创建一个订单。 那下面这个请求返回的是什么呢? GET http://www.example.com/customers/33245/orders 可能是一个编号为33245的用户所创建或拥有的订单列表。注意:我们可以屏蔽对该URI进行DELETE或PUT请求,因为它的操作对象是一个集合。
示例三、更多
POST http://www.example.com/customers/33245/orders/8769/lineitems 可能是(为编号33245的用户)增加一个编号为8769的订单条目。没错!如果使用GET方式请求这个URI,则会返回这个订单的所有条目。但是,如果这些条目与用户信息无关,我们将会提供POST www.example.com/orders/8769/lineitems这个URI。 从返回的这些条目来看,指定的资源可能会有多个URIs,所以我们可能也需要要提供这样一个URI GET http://www.example.com/orders/8769,用来在不知道用户ID的情况下根据订单ID来查询订单。 更进一步: GET http://www.example.com/customers/33245/orders/8769/lineitems/1 可能只返回同个订单中的第一个条目。
现在你应该理解什么是分层结构了。它们并不是严格的规则,只是为了确保在你的服务中这些强制的结构能够更容易被用户所理解。与所有软件开发中的技能一样,命名是成功的关键。
更多API资源的URIs。这里有一些APIs的例子:
- Twitter: https://dev.twitter.com/docs/api
- Facebook: http://developers.facebook.com/docs/reference/api/
- LinkedIn: https://developer.linkedin.com/apis
反例一、
一些serivices往往使用单一的URI来指定服务接口,然后通过查询参数来指定HTTP请求的动作。例如,要更新编号12345的用户信息,带有JSON body的请求可能是这样: GET http://api.example.com/services?op=update_customer&id=12345&format=json 尽管上面URL中的"services"的这个节点是一个名词,但这个URL不是自解释的,因为对于所有的请求而言,该URI的层级结构都是一样的。此外,它使用GET作为HTTP动词来执行一个更新操作,这简直就是反人类(甚至是危险的)。 下面是另外一个更新用户的操作的例子: GET http://api.example.com/update_customer/12345 以及它的一个变种: GET http://api.example.com/customers/12345/update 你会经常看到在其他开发者的服务套件中有很多这样的用法。可以看出,这些开发者试图去创建RESTful的资源名称,而且已经有了一些进步。但是你仍然能够识别出URL中的动词短语。注意,在这个URL中我们不需要"update"这个词,因为我们可以依靠HTTP动词来完成操作。
下面这个URL正好说明了这一点: PUT http://api.example.com/customers/12345/update 这个请求同时存在PUT和"update",这会对消费者产生迷惑!这里的"update"指的是一个资源吗?
3、复数
通常我们都会选择使用复数命名,以使得你的API URI在所有的HTTP方法中保持一致。原因是基于这样一种考虑:customers是服务套件中的一个集合,而ID33245的这个用户则是这个集合中的其中一个。
这意味着你的每个根资源只需要两个基本的URL就可以了,一个用于创建集合内的资源,另一个用来根据标识符获取、更新和删除资源。
四、返回表征
4.1、响应格式XML和JSON
建议默认支持json,并且,除非花费很惊人,否则就同时支持json和xml。在理想情况下,让使用者仅通过改变扩展名.xml和.json来切换类型。此外,对于支持ajax风格的用户界面,一个被封装的响应是非常有帮助的。提供一个被封装的响应,在默认的或者有单独扩展名的情况下,例如:.wjson和.wxml,表明客户端请求一个被封装的json或xml响应(请参见下面的封装响应)。
“标准”中对json的要求很少。并且这些需求只是语法性质的,无关内容格式和布局。换句话说,REST服务端调用的json响应是协议的一部分——在标准中没有相关描述。更多关于json数据格式可以在http://www.json.org/上找到。
关于REST服务中xml的使用,xml的标准和约定除了使用语法正确的标签和文本外没有其它的作用。特别地,命名空间不是也不应该是被使用在REST服务端的上下文中。xml的返回更类似于json——简单、容易阅读,没有模式和命名空间的细节呈现——仅仅是数据和链接。如果它比这更复杂的话,参看本节的第一段——使用xml的成本是惊人的。鉴于我们的经验,很少有人使用xml作为响应。在它被完全淘汰之前,这是最后一个可被肯定的地方。
示例
当url中没有包含格式说明时,服务端应该返回默认格式的表征(假设为JSON)。例如: GET http://www.example.com/customers/12345 GET http://www.example.com/customers/12345.json 以上两者返回的ID为12345的customer数据均为JSON格式,这是服务端的默认格式。 GET http://www.example.com/customers/12345.xml
4.2、资源通过链接的可发现性(HATEOAS续)
REST指导原则之一(根据统一接口原则)是application的状态通过hypertext(超文本)来传输。这就是我们通常所说的Hypertext As The Engine of Application State (即HATEOAS,用超文本来作为应用程序状态机),我们在“REST是什么”一节中也提到过。
根据Roy Fielding在他的博客中的描述(http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertextdriven),REST接口中最重要的部分是超文本的使用。此外,他还指出,在给出任何相关的信息之前,一个API应该是可用和可理解的。也就是说,一个API应当可以通过其链接导航到数据的各个部分。不建议只返回纯数据。
不过目前的业界先驱们并没有经常采用这种做法,这反映了HATEOAS仅仅在成熟度模型中的使用率更高。纵观众多的服务体系,它们大多返回更多的数据,而返回的链接却很少(或者没有)。这是违背Fielding的REST约定的。Fielding说:“信息的每一个可寻址单元都携带一个地址……查询结果应该表现为一个带有摘要信息的链接清单,而不是对象数组。”
另一方面,简单粗暴地将整个链接集合返回会大大影响网络带宽。在实际情况中,根据所需的条件或使用情况,API接口的通信量要根据服务器响应中超文本链接所包含的“摘要”数量来平衡。
同时,充分利用HATEOAS可能会增加实现的复杂性,并对服务客户端产生明显的负担,这相当于降低了客户端和服务器端开发人员的生产力。因此,当务之急是要平衡超链接服务实践和现有可用资源之间的问题。
超链接最小化的做法是在最大限度地减少客户端和服务器之间的耦合的同时,提高服务端的可用性、可操纵性和可理解性。这些最小化建议是:通过POST创建资源并从GET请求返回集合,对于有分页的情况后面我们会提到。
最小化链接推荐
在create的用例中,新建资源的URI(链接)应该在Location响应头中返回,且响应主体是空的——或者只包含新建资源的ID。
对于从服务端返回的表征集合,每个表征应该在它的链接集合中携带一个最小的“自身”链接属性。为了方便分页操作,其它的链接可以放在一个单独的链接集合中返回,必要时可以带有“第一页”、“上一页”、“下一页”、“最后一页”等信息。
参照下文链接格式部分的例子获取更多信息。
链接格式
参照整个链接格式的标准,建议遵守一些类似Atom、AtomPub或Xlink的风格。JSON-LD也不错,但并没有被广泛采用(如果曾经被用过)。目前业内最普遍的方式是使用带有"rel"元素和包含资源完整URI的"href"元素的Atom链接格式,不包含任何身份验证或查询字符串参数。"rel"元素可以包含标准值"alternate"、"related"、"self"、"enclosure"和"via",还有分页链接的“第一页”、“上一页”、“下一页”,“最后一页”。在需要时可以自定义并添加使用它们。
一些XML Atom格式的概念对于用JSON格式表示的链接来说是无用的。例如,METHOD属性对于一个RESTful资源来说是不需要的,因为对于一个给定的资源,在所有支持的HTTP方法(CRUD行为)中,资源的URI都是相同的——所以单独列出这些是没有必要的。
示例一
下面是调用创建新资源的请求后的响应: POST http://api.example.com/users 下面是响应头集合中带有创建新资源的URI的Location部分: HTTP/1.1 201 CREATED Status: 201 Connection: close Content-Type: application/json; charset=utf-8 Location: http://api.example.com/users/12346 返回的body可以为空,或者包含一个被封装的响应
示例二、
下面的例子通过GET请求获取一个不包含分页的表征集合的JSON响应: { "data": [ { "user_id": "42", "name": "Bob", "links": [ { "rel": "self", "href": "http://api.example.com/users/42" } ] }, { "user_id": "22", "name": "Frank", "links": [ { "rel": "self", "href": "http://api.example.com/users/22" } ] }, { "user_id": "125", "name": "Sally", "links": [ { "rel": "self", "href": "http://api.example.com/users/125" } ] } ] } 注意,links数组中的每一项都包含一个指向“自身(self)”的链接。该数组还可能还包含其它关系,如children、parent等。 最后一个例子是通过GET请求获取一个包含分页的表征集合的JSON响应(每页显示3项),我们给出第三页的数据: { "data": [ { "user_id": "42", "name": "Bob", "links": [ { "rel": "self", "href": "http://api.example.com/users/42" } ] }, { "user_id": "22", "name": "Frank", "links": [ { "rel": "self", "href": "http://api.example.com/users/22" } ] }, { "user_id": "125", "name": "Sally", "links": [ { "rel": "self", "href": "http://api.example.com/users/125" } ] } ], "links": [ { "rel": "first", "href": "http://api.example.com/users?offset=0&limit=3" }, { "rel": "last", "href": "http://api.example.com/users?offset=55&limit=3" }, { "rel": "previous", "href": "http://api.example.com/users?offset=3&limit=3" }, { "rel": "next", "href": "http://api.example.com/users?offset=9&limit=3" } ] }
在这个例子中,响应中用于分页的links集合中的每一项都包含一个指向“自身(self)”的链接。这里可能还会有一些关联到集合的其它链接,但都与分页本身无关。简而言之,这里有两个地方包含links。一个就是data对象中所包含的集合(这个也是接口要返回给客户端的数据表征集合),其中的每一项至少要包括一个指向“自身(self)”的links集合;另一个则是一个单独的对象links,其中包括和分页相关的链接,该部分的内容适用于整个集合。
对于通过POST请求创建资源的情况,需要在响应头中包含一个关联新建对象链接的Location。
4.3、封装响应
服务器可以在响应中同时返回HTTP状态码和body。有许多JavaScript框架没有把HTTP状态响应码返回给最终的开发者,这往往会导致客户端无法根据状态码来确定具体的行为。此外,虽然HTTP规范中有很多种响应码,但是往往只有少数客户端会关心这些——通常大家只在乎"success"、"error"或"failture"。因此,将响应内容和响应状态码封装在包含响应信息的表征中,是有必要的。
OmniTI 实验室有这样一个提议,它被称为JSEND响应。更多信息请参考http://labs.omniti.com/labs/jsend。另外一个提案是由Douglas Crockford提出的,可以查看这里http://www.json.org/JSONRequest.html。
这些提案在实践中并没有完全涵盖所有的情况。基本上,现在最好的做法是依照以下属性封装常规(非JSONP)响应:
- code——包含一个整数类型的HTTP响应状态码。
- status——包含文本:"success","fail"或"error"。HTTP状态响应码在500-599之间为"fail",在400-499之间为"error",其它均为"success"(例如:响应状态码为1XX、2XX和3XX)。
- message——当状态值为"fail"和"error"时有效,用于显示错误信息。参照国际化(il8n)标准,它可以包含信息号或者编码,可以只包含其中一个,或者同时包含并用分隔符隔开。
- data——包含响应的body。当状态值为"fail"或"error"时,data仅包含错误原因或异常名称,不推荐使用data,推荐使用moreInfo。
- moreInfo——当状态值为"fail"和"error"时有效,用于显示更多错误信息。主要用于分析问题使用。
下面是一个返回success的封装响应:
{ "code": 200, "status": "success", "data": { "lacksTOS": false, "invalidCredentials": false, "authToken": "4ee683baa2a3332c3c86026d" } }
返回error的封装响应:
{ "code": 401, "status": "error", "message": "token is invalid", "moreInfo": "UnauthorizedException" }
4.3、处理跨域问题
我们都听说过有关浏览器的同源策略或同源性需求。它指的是浏览器只能请求当前正在显示的站点的资源。例如,如果当前正在显示的站点是www.Example1.com,则该站点不能对www.Example.com发起请求。显然这会影响站点访问服务器的方式。
目前有两个被广泛接受的支持跨域请求的方法:JSONP和跨域资源共享(CORS)。JSONP或“填充的JSON”是一种使用模式,它提供了一个方法请求来自不同域中的服务器的数据。其工作方式是从服务器返回任意的JavaScript代码,而不是JSON。客户端的响应由JavaScript解析器进行解析,而不是直接解析JSON数据。另外,CORS是一种web浏览器的技术规范,它为web服务器定义了一种方式,从而允许服务器的资源可以被不同域的网页访问。CORS被看做是JSONP的最新替代品,并且可以被所有现代浏览器支持。因此,不建议使用JSONP。任何情况下,推荐选择CORS。
支持CORS
在服务端实现CORS很简单,只需要在发送响应时附带HTTP头,例如:
Access-Control-Allow-Origin: *
只有在数据是公共使用的情况下才会将访问来源设置为"*"。大多数情况下,Access-Control-Allow-Origin头应该指定哪些域可以发起一个CORS请求。只有需要跨域访问的URL才设置CORS头。
Access-Control-Allow-Origin: http://example.com:8080 http://foo.example.com
以上Access-Control-Allow-Origin头中,被设置为只允许受信任的域可以访问。
Access-Control-Allow-Credentials: true
只在需要时才使用上面这个header,因为如果用户已经登录的话,它会同时发送cookies/sessions。
这些headers可以通过web服务器、代理来进行配置,或者从服务器本身发送。不推荐在服务端实现,因为很不灵活。或者,可以使用上面的第二种方式,在web服务器上配置一个用空格分隔的域的列表。更多关于CORS的内容可以参考这里:http://enable-cors.org/。
支持JSONP【不推荐】
JSONP通过利用GET请求避开浏览器的限制,从而实现对所有服务的调用。其工作原理是请求方在请求的URL上添加一个字符串查询参数(例如:jsonp=”jsonp_callback”),其中“jsonp”参数的值是JavaScript函数名,该函数在有响应返回时将会被调用。
由于GET请求中没有包含请求体,JSONP在使用时有着严重的局限性,因此数据必须通过字符串查询参数来传递。同样的,为了支持PUT,POST和DELETE方法,HTTP方法必须也通过字符串查询参数来传递,类似_method=POST这种形式。像这样的HTTP方法传送方式是不推荐使用的,这会让服务处于安全风险之中。
JSONP通常在一些不支持CORS的老旧浏览器中使用,如果要改成支持CORS的,会影响整个服务器的架构。或者我们也可以通过代理来实现JSONP。总之,JSONP正在被CORS所替代,我们应该尽可能地使用CORS。
为了在服务端支持JSONP,在JSONP字符串查询参数传递时,响应必须要执行以下这些操作:
- 响应体必须封装成一个参数传递给jsonp中指定的JavaScript函数(例如:jsonp_callback("<JSON response body>"))。
- 始终返回HTTP状态码200(OK),并且将真实的状态作为JSON响应中的一部分返回。
另外,响应体中常常必须包含响应头。这使得JSONP回调方法需要根据响应体来确定响应处理方式,因为它本身无法得知真实的响应头和状态值。
下面的例子是按照上述方法封装的一个返回error状态的jsonp(注意:HTTP的响应状态是200):
jsonp_callback("{\'code\':\'404\', \'status\':\'error\',\'headers\':[],\'message\':\'resource XYZ not found\',\'data\':\'NotFoundException\'}")
成功创建后的响应类似于这样(HTTP的响应状态仍是200):
jsonp_callback("{\'code\':\'201\', \'status\':\'error\',\'headers\':[{\'Location\':\'http://www.example.com/customers/12345\'}],\'data\':\'12345\'}")
五、查询,过滤和分页
对于大数据集,从带宽的角度来看,限制返回的数据量是非常重要的。而从UI处理的角度来看,限制数据量也同样重要,因为UI通常只能展现大数据集中的一小部分数据。在数据集的增长速度不确定的情况下,限制默认返回的数据量是很有必要的。以Twitter为例,要获取某个用户的推文(通过个人主页的时间轴),如果没有特别指定,请求默认只会返回20条记录,尽管系统最多可以返回200条记录。
除了限制返回的数据量,我们还需要考虑如何对大数据集进行“分页”或下拉滚动操作。创建数据的“页码”,返回大数据列表的已知片段,然后标出数据的“前一页”和“后一页”——这一行为被称为分页。此外,我们可能也需要指定响应中将包含哪些字段或属性,从而限制返回值的数量,并且我们希望最终能够通过特定值来进行查询操作,并对返回值进行排序。
有两种主要的方法来同时限制查询结果和执行分页操作。首先,我们可以建立一个索引方案,它可以以页码为导向(请求中要给出每一页的记录数及页码),或者以记录为导向(请求中直接给出第一条记录和最后一条记录)来确定返回值的起始位置。举个例子,这两种方法分别表示:“给出第五页(假设每页有20条记录)的记录”,或“给出第100到第120条的记录”。
服务端将根据运作机制来进行切分。有些UI工具,比如Dojo JSON会选择模仿HTTP规范使用字节范围。如果服务端支持out of box(即开箱即用功能),则前端UI工具和后端服务之间无需任何转换,这样使用起来会很方便。
下文将介绍一种方法,既能够支持Dojo这样的分页模式(在请求头中给出记录的范围),也能支持使用字符串查询参数。这样一来服务端将变得更加灵活,既可以使用类似Dojo一样先进的UI工具集,也可以使用简单直接的链接和标签,而无需再为此增加复杂的开发工作。但如果服务不直接支持UI功能,可以考虑不要在请求头中给出记录范围。
要特别指出的是,我们并不推荐在所有服务中使用查询、过滤和分页操作。并不是所有资源都默认支持这些操作,只有某些特定的资源才支持。服务和资源的文档应当说明哪些接口支持这些复杂的功能。
5.1、结果限制
“给出第3到第55条的记录”,这种请求数据的方式和HTTP的字节范围规范更一致,因此我们可以用它来标识Range header。而“从第2条记录开始,给出最多20条记录”这种方式更易于阅读和理解,因此我们通常会用字符串查询参数的方式来表示。
综上所述,推荐既支持使用HTTP Range header,也支持使用字符串查询参数——offset(偏移量)和limit(限制),然后在服务端对响应结果进行限制。注意,如果同时支持这两种方式,那么字符串查询参数的优先级要高于Range header。
关键是,字符串查询参数看起来更加清晰易懂,在构建和解析时更加方便。而Range header则更多是由机器来使用(偏向于底层),它更加符合HTTP使用规范。
总之,解析Range header的工作会增加复杂度,相应的客户端在构建请求时也需要进行一些处理。而使用单独的limit和offset参数会更加容易理解和构建,并且不需要对开发人员有更多的要求。
用范围标记进行限制-HTTP Range header
当用HTTP header而不是字符串查询参数来获取记录的范围时,Ranger header应该通过以下内容来指定范围【请求头设置如下】:
Range: items=0-24
注意记录是从0开始的连续字段,HTTP规范中说明了如何使用Range header来请求字节。也就是说,如果要请求数据集中的第一条记录,范围应当从0开始算起。上述的请求将会返回前25个记录,假设数据集中至少有25条记录。
而在服务端,通过检查请求的Range header来确定该返回哪些记录。只要Range header存在,就会有一个简单的正则表达式(如"items=(\\d+)-(\\d+)")对其进行解析,来获取要检索的范围值。
用字符串查询参数进行限制【推荐】
字符串查询参数被作为Range header的替代选择,它使用offset和limit作为参数名,其中offset代表要查询的第一条记录编号(与上述的用于范围标记的items第一个数字相同),limit代表记录的最大条数。下面的例子返回的结果与上述用范围标记的例子一致:
GET http://api.example.com/resources?offset=0&limit=25
Offset参数的值与Range header中的类似,也是从0开始计算。Limit参数的值是返回记录的最大数量。当字符串查询参数中未指定limit时,服务端应当给出一个缺省的最大limit值,不过这些参数的使用都需要在文档中进行说明。
基于范围的响应
对一个基于范围的请求来说,无论是通过HTTP的Range header还是通过字符串查询参数,服务端都应该有一个Content-Range header来响应,以表明返回记录的条数和总记录数:
Content-Range: items 0-24/66
注意这里的总记录数(如本例中的66)不是从0开始计算的。如果要请求数据集中的最后几条记录,Content-Range header的内容应该是这样:
Content-Range: items 40-65/66
根据HTTP的规范,如果响应时总记录数未知或难以计算,也可以用星号("*")来代替(如本例中的66)。本例中响应头也可这样写:
Content-Range: items 40-65/*
不过要注意,Dojo或一些其它的UI工具可能不支持该符号。
5.2、分页
上述方式通过请求方指定数据集的范围来限制返回结果,从而实现分页功能。上面的例子中一共有66条记录,如果每页25条记录,要显示第二页数据,Range header的内容如下:
Range: items=25-49
同样,用字符串查询参数表示如下:
GET …?offset=25&limit=25
服务端会相应地返回一组数据,附带的Content-Range header内容如下:
Content-Range: 25-49/66
在大部分情况下,这种分页方式都没有问题。但偶尔会有这种情况,就是要返回的记录数量无法直接表示成数据集中的行号。还有就是有些数据集的变化很快,不断会有新的数据插入到数据集中,这样必然会导致分页出现问题,一些重复的数据可能会出现在不同的页中。
按日期排列的数据集(例如Twitter feed)就是一种常见的情况。虽然你还是可以对数据进行分页,但有时用"after"或"before"这样的关键字并与Range header(或者与字符串查询参数offset和limit)配合来实现分页,看起来会更加简洁易懂。
例如,要获取给定时间戳的前20条评论:
GET http://www.example.com/remarks/home_timeline?after=<timestamp>
Range: items=0-19
GET http://www.example.com/remarks/home_timeline?before=<timestamp>
Range: items=0-19
用字符串查询参数表示为:
GET http://www.example.com/remarks/home_timeline?after=<timestamp>&offset=0&limit=20
GET http://www.example.com/remarks/home_timeline?before=<timestamp>&offset=0&limit=20
有关在不同情况对时间戳的格式化处理,请参见下文的“日期/时间处理”。
如果请求时没有指定要返回的数据范围,服务端返回了一组默认数据或限定的最大数据集,那么服务端同时也应该在返回结果中包含Content-Range header来和客户端进行确认。以上面个人主页的时间轴为例,无论客户端是否指定了Range header,服务端每次都只返回20条记录。此时,服务端响应的Content-Range header应该包含如下内容:
Content-Range: 0-19/4125
或 Content-Range: 0-19/*
5.3、结果的过滤和排序
针对返回结果,还需要考虑如何在服务端对数据进行过滤和排列,以及如何按指定的顺序对子数据进行检索。这些操作可以与分页、结果限制,以及字符串查询参数filter和sort等相结合,可以实现强大的数据检索功能。
再强调一次,过滤和排序都是复杂的操作,不需要默认提供给所有的资源。下文将介绍哪些资源需要提供过滤和排序。
过滤
在本文中,过滤被定义为“通过特定的条件来确定必须要返回的数据,从而减少返回的数量”。如果服务端支持一套完整的比较运算符和复杂的条件匹配,过滤操作将变得相当复杂。不过我们通常会使用一些简单的表达式,如starts-with(以...开始)或contains(包含)来进行匹配,以保证返回数据的完整性。
在我们开始讨论过滤的字符串查询参数之前,必须先明白为什么要使用单个参数而不是多个字符串查询参数。从根本上来说是为了减少参数名称的冲突。我们已经有offset、limit和sort(见下文)参数了。如果可能的话还会有jsonp、format标识符,或许还会有after和before参数,这些都是在本文中提到过的字符串查询参数。字符串查询中使用的参数越多,就越可能导致参数名称的冲突,而使用单个过滤参数则会将冲突的可能性降到最低。
此外,从服务端也很容易仅通过单个的filter参数来判断请求方是否需要数据过滤功能。如果查询需求的复杂度增加,单个参数将更具有灵活性——可以自己建立一套功能完整的查询语法(详见下文OData注释或访问http://www.odata.org)。
通过引入一组常见的、公认的分隔符,用于过滤的表达式可以以非常直观的形式被使用。用这些分隔符来设置过滤查询参数的值,这些分隔符所创建的参数名/值对能够更加容易地被服务端解析并提高数据查询的性能。目前已有的分隔符包括用来分隔每个过滤短语的竖线("|")和用来分隔参数名和值的双冒号("::")。这套分隔符足够唯一,并适合大多数情况,同时用它来构建的字符串查询参数也更加容易理解。下面将用一个简单的例子来介绍它的用法。假设我们想要给名为“Todd”的用户们发送请求,他们住在丹佛,有着“Grand Poobah”之称。用字符串查询参数实现的请求URI如下:
GET http://www.example.com/users?filter="name::todd|city::denver|title::grand poobah"
双冒号("::")分隔符将属性名和值分开,这样属性值就能够包含空格——服务端能更容易地从属性值中解析出分隔符。
注意查询参数名/值对中的属性名要和服务端返回的属性名相匹配。
简单而有效。有关大小写敏感的问题,要根据具体情况来看,但总的来说,在不用关心大小写的情况下,过滤功能可以很好地运作。若查询参数名/值对中的属性值未知,你也可以用星号("*")来代替。
除了简单的表达式和通配符之外,若要进行更复杂的查询,你必须要引入运算符。在这种情况下,运算符本身也是属性值的一部分,能够被服务端解析,而不是变为属性名的一部分。当需要复杂的query-language-style(查询语言风格)功能时,可参考Open Data Protocol (OData) Filter System Query Option说明中的查询概念(详见http://www.odata.org/documentation/uriconventions#FilterSystemQueryOption)。
排序
排序决定了从服务端返回的记录的顺序。也就是对响应中的多条记录进行排序。
同样,我们这里只考虑一些比较简单的情况。推荐使用排序字符串查询参数,它包含了一组用分隔符分隔的属性名。具体做法是,默认对每个属性名按升序排列,如果属性名有前缀"-",则按降序排列。用竖线("|")分隔每个属性名,这和前面过滤功能中的参数名/值对的做法一样。
举个例子,如果我们想按用户的姓和名进行升序排序,而对雇佣时间进行降序排序,请求将是这样的:
GET http://www.example.com/users?sort=last_name|first_name|-hire_date
再次强调一下,查询参数名/值对中的属性名要和服务端返回的属性名相匹配。此外,由于排序操作比较复杂,我们只对需要的资源提供排序功能。如果需要的话也可以在客户端对小的资源集合进行排列。
六、服务版本管理
版本会增加API的复杂度,并同时可能会对客户端产生一些影响。因此,在API的设计中要尽量避免多个不同的版本。
不支持版本,不将版本控制作为糟糕的API设计的依靠。如果你在APIs的设计中引入版本,这迟早都会让你抓狂。由于返回的数据通过JSON来呈现,客户端会由于不同的版本而接收到不同的属性。这样就会存在一些问题,如从内容本身和验证规则方面改变了一个已存在的属性的含义。
当然,我们无法避免API可能在某些时候需要改变返回数据的格式和内容,而这也将导致消费端的一些变化,我们应当避免进行一些重大的调整。将API进行版本化管理是避免这种重大变化的一种有效方式。
6.1、通过内容协商支持版本管理
1、传统方式【URI中显式添加版本号,路径上或者参数上】
以往,版本管理通过URI本身的版本号来完成,客户端在请求的URI中标明要获取的资源的版本号。事实上,许多大公司如Twitter、Yammer、Facebook、Google等经常在他们的URI里使用版本号。甚至像WSO2这样的API管理工具也会在它的URLs中要求版本号。
Twilio /2010-04-01/Accounts/
salesforce.com /services/data/v20.0/sobjects/Account
Facebook ?v=1.0
1、如何使用REST以实用的方式考虑版本号?
从不在没有版本的情况下发布API。 使版本成为强制性的。 指定带有\'v\'前缀的版本。 将其一直移动到URL中的左侧,使其具有最高范围(例如/ v1 / dogs)。
使用简单的序数。 不要像v1.2那样使用点符号,因为它意味着版本控制的粒度与API不兼容 - 它是一个接口而不是实现。 坚持使用v1,v2等。
你应该维护多少个版本? 至少维护一个版本。 您应该维护多长时间? 在淘汰版本之前,给开发人员至少一个周期做出反应。 有时那是6个月; 有时它是2年。 这取决于您的开发人员的开发平台,应用程序类型和应用程序用户。 例如,移动应用程序比Web应用程序需要更长的时间。
2、版本和格式应该放在URL还是header中
遵循的简单规则:如果它更改了您编写的逻辑以处理响应,请将其放在URL中,以便您可以轻松查看。如果它没有更改每个响应的逻辑,例如OAuth信息,请将其放在标题中。
把版本号嵌入到API中,例如developer.github.com/v3/media/,访问操作对应版本号下的资源。这种显示的表示版本号的好处是可以很直观的展示当前api版本号。缺点是违背RESTful架构的原则,理论上一个URI对应服 务器一个特定的资源,添加版本号则会混淆版本和资源的概念,而且会让整个 架构变得混乱,增加日后维护的成本。 还有一种是把版本号作为参数请求api 获取操作对应版本号的资源,例如www.demo.com/list?version=2。
2、面向REST原则的版本管理
在API请求header中添加Accept字段。Accept的作用是客户端指出响应可以接受的媒体类型
方式一、使用 Accept: application/json; version=1【没有找到spring的实现方式】
面向REST原则,版本管理技术飞速发展。因为它不包含HTTP规范中内置的header,也不支持仅当一个新的资源或概念被引入时才应该添加新URI的观点——即版本不是表现形式的变化。另一个反对的理由是资源URI是不会随时间改变的,资源就是资源。
URI应该能简单地识别资源——而不是它的“形状”(状态)。另一个就是必须指定响应的格式(表征)。还有一对HTTP headers:Accept 和 Content-Type。Accept header允许客户端指定所希望或能支持的响应的媒体类型(一种或多种)。Content-Type header可分别被客户端和服务端用来指定请求或响应的数据格式。
例如,要获取一个user的JSON格式的数据:
#Request: GET http://api.example.com/users/12345 Accept: application/json; version=1 #Response: HTTP/1.1 200 OK Content-Type: application/json; version=1 {"id":"12345", "name":"Joe DiMaggio"}
现在,我们对同一资源请求版本2的数据:
#Request: GET http://api.example.com/users/12345 Accept: application/json; version=2 #Response: HTTP/1.1 200 OK Content-Type: application/json; version=2 {"id":"12345", "firstName":"Joe", "lastName":"DiMaggio"}
Accept header被用来表
以上是关于001-web api design-概述简介的主要内容,如果未能解决你的问题,请参考以下文章
015-ant design pro advanced 使用 API 文档工具
web service001——web service简单认识