在公开API时,需要以未知的第三方能顺利调用为前提,做好相关文档的公开工作。在设计API时,也需要设计者时刻铭记将API设计得易于理解,便于使用。另外,有时候还必须对用户的登录以及访问加以控制。当变更API的设计规范时,还需要顾忌那些仍在使用变更前规范的用户,制定应对策略。有时甚至还需要考虑是否公开IOS以Android等移动客户端的SDK。
公司内部多个系统的集成
如今,虽然公司内部业务信息化的案例不在少数,但由于这样的信息化系统需要根据公司内部的需求等开发或改进,因此不同时期搭建的信息系统,不同岗位搭建的系统系统杂乱无章同时存在的情况非常多。这种情况下,如果各系统进行集成,或各系统之间直接相互访问数据库,那么一处地方的变更就会引起多米诺骨牌效应,引起其他系统发生不良反应,这样的风险也在不断增加。
针对这种情况,通过使用Web API将各个系统集成,就能将一处地方的改变对其他系统带来的影响控制在最小范围内,加上Web API技术广为流传,很多人能驾轻就熟,集成的工作也更加容易。
Web API需要设计优美的几个理由
- 易于使用
- 便于更改
- 健壮性好
- 不怕公之于众
1:易于使用
首先,很多情况下设计Web API并非只是为了自己使用,且不说广泛公开的Web API,即使是某些移动应用的API,服务器端跟客户端分别由不同开发人员负责的情况也很多。在这种情况下,API设计的好坏,易用性,会对开发周期以及开发期间承受的压力造成直接的影响。
2:便于修改
Web服务以及Web系统几乎是每时每刻都在变化的,也就是说,保持公开时的状态连续使用两三年的情况非常罕见,因此,当我们的服务发生变化时,作为其接口的API也不得不随之改变。
但是,公开的API在很多情况下会被与自己无关的第三方调用,如果这时候突然变更API的设计规范,很有可能造成这些第三方开发的系统,服务等一下子变得不可用,这无疑是API提供者需要极力避免的情况。
移动应用的情况下,什么时候更新应用往往由用户自己掌控,即使将客户端应用更新到最新版本,也依然存在使用老版本的用户,如果此时突然变更API的设计规范,就会使得老版本的应用突然无法使用。
3:健壮性好
由于谁都能够访问API,因此会设计到安全问题,由于API同Web站点一样使用了HTTP协议,因此会面临同Web站点一样的安全问题,除此之外,开发人员还得考虑API特有的安全问题。
4:不怕公之于众
API同一般的Web站点,Web服务不同,主要面向开发人员。众所周知,开发人员往往喜欢评价其他开发人员写的代码,接口等成果。因此,如果公开的API在其他开发人员眼里显得丑陋,没有美感的话,该服务提供者的技术水平也会受到质疑。
什么是优秀的URI设计
要谈论 什么是优秀的URI设计,有一种非常重要的规则,就是:容易记忆,URI包含的功能一目了然。
根据这个原则,我们总结除了下面几条规则:
- 短小便于输入的URI
- 人人可以读懂的URI
- 没有大小写混写的URI
- 修改方便的URI
- 不会暴露服务器架构的URI
- 规则统一的URI
1:短小便于输入的URI
此规则意味着URI简单易记,而冗长的URI往往就会混有无用,重复的内容。让我们来看一个例子。
http://api.example.com/services/api/search
该URI中包含了api,search等单词,可以知道这是一个用于检测某种信息的API。同时我们又发现主机名和路径中都包含了api单词,就显得有点重复,另外,该URI中还包含了service这类表示雷同概念的单词,实际上,我们可以修改成下面这种:
http://api.example.com/search
通过该URI,我们也能得知这是一个用于检测某种信息的API。在表示的信息量相同的情况下,使用短小,简单的表达方式更易于理解和记忆,并能减少输入时的错误。
2: 人人可以读懂的URI
此规则就是说一看到某一个URI,即使没有其他提示,也能理解其用途。下面我们举一个意思不明确的例子:
http://api.example.com/sr/u/
URI里面包含的api一词,可以得知它是某API的URI,但sr和u是什么意思就不得而知了,有可能是某个单词的缩写,具体什么意思可能就需要跟URI的设计人员进行进一步的沟通才能知道意思。
为了避免写出这种难以理解的URI,首先就要做到不轻易使用缩写方式。除了缩写,我们还要注意下面2点。
- 要使用API里面常用的英语单词。
- 是要尽可能地避免拼写错误。
易于理解的URI还可以减轻用户编写访问API的代码时的负担,因为如果从URI就知道API 的用途,那么开发人员在阅读访问该API的代码时,就可以不用每次都去翻阅API的相关文档,从而提高了工作效率。
3:没有大小写混写的URI
此规则指的是不要使用如下所示的大小写混写的URI, 一般建议全部使用小写字母的形式。
http://api.example.com/Users/12345
http://example.com/API/GetUserName
大小写字母混用会造成API难以理解,容易让人搞错,因此需要统一为全部大写或全部小写,一般标准的做法是全部小写。
4:修改方便的URI
此规则指的是能将某个URI非常容易地修改成另一个URI。假如我们需要获取某种商品,该API的端点如下所示:
http://api.example.com/vi/items/123456
我们从以上URI就能很直观的看出该商品ID为123456,并且可以猜测到只要修改这一ID,我们就可以访问到其他商品的信息。
5:不会暴露服务器端架构的URI
服务器端架构信息包括使用了什么服务软件,哪种开发语言以及服务器端的目录和系统架构等。下面我们举个例子:
htttp://api.example.com/cgi-bin/get_user.php?user=100
从上面的URI中就知道API可能是用PHP编写并以GCI的形式运行,这些信息对API用户来说是多余的,因为对他们来说,他们要的是返回的数据,至于用什么语言编写是没有区别的。从另一方面来讲,也会导致那些企图利用服务器漏洞的黑客来进行攻击。
Web应用里也同样无需在URI中体现服务器端的架构和目录结构,对Web API而言,URI应体现功能,数据结构和含义,而不是服务器端如何运作等信息。
6:规则统一的URI
对外提供API时,仅用一个或一种规则来描述端点的情况很少见,大多数都会公开多个API端点。那么我们就举个例子
获取好友信息:http://api.example.com/friends?id=100
发送消息:http://api.example.com/friend/100/message
在以上示例中,获取好友信息的API中使用了friends这样的复数形式,ID信息通过查询参数进行传递,而发送消息的API里却使用了friend ,message这样的单数形式,ID参数则通过URI路径进行指定,这么做无疑会让人觉得杂乱无章,一点也不统一,不但视觉上不美观,而且在客户端实现时还会造成混乱,成为制造麻烦的源头。
如果使用如下所示的规则统一的URI,便会更容易理解。
获取好友信息:http://api.example.com/friends?id=100
发送消息:http://api.example.com/friends/100/message
HTTP方法
URI和HTTP方法之间的关系可以认为是操作对象和操作方法的关系。如果把URI当做API(HTTP)的操作对象,HTTP方法则表示进行怎样的操作。
开发Web应用时,一种普遍的做法是通过GET方法获取服务器端的信息,而用POST方法修改服务器端的信息。
由于HTTP4.0只允许使用POST和GET方法,因此在开发普通的Web应用时,多数情况下只会使用到GET和POST方法,但在HTTP协议中定义了其他的方法,如下所示:
方法名 | 说明 |
GET | 获取资源 |
POST | 新增资源 |
PUT | 更新已有资源 |
DELETE | 删除资源 |
PUTCH | 更新部分资源 |
HEAD | 获取资源的元信息 |
1:GET方法
GET方法是访问Web最常见的方法,表示“获取信息”。GET方法一般用于获取URI制定的资源,因此,当使用GET方法访问时,一般不会修改服务器上已有的资源(当然,已读/未读,最后访问日期等资源属性会因为GET操作而自我更新,属于例外)。
2:POST方法
POST方法和GET方法成对使用,一般认为GET方法用于获取信息,而POST方法则用于更新信息,但其实这样的理解仍然存在一些偏差。
POST方法的初衷是发送附属于指定URI的新建资源信息,简而言之,该方法用于向服务器端注册新建的资源。信息的更新,删除等操作则通过其他HTTP方法来完成。
3:PUT方法
PUT方法和POST方法相同,都可用于对服务器端的信息进行更新。
如果URI资源已经存在,PUT操作就意味着对该资源进行更新。虽然HTTP协议定义了当所指定的资源不存在时,可以通过PUT操作发送数据生成新的资源。但API一般只用PUT方法来更新数据,使用POST来生成新的资源。
4:DELETE方法
DELETE方法用于删除指定的资源,具体便是删除指定URI所描述的资源。
5:PATCH方法
PATCH方法和PUT方法相同,都用于更新指定的资源。从PATCH一词就能想到该方法所表示的更新并不是更新资源的全部信息,而是只更新资源的一部分信息。
PUT会用发送的数据替换原有的资源信息,而PATCH方法只会更新资源中的部分信息。例如,当遇到多个值组成的高达1MB的数据时,如果只想更新该数据中的一小部分信息,如果用PUT方法,就会在每次更新时发送所有的1MB数据,效率很低,使用PATCH方法,则只需发送期望更新的那一小部分数据即可。
数据格式的指定方法
目前的话,JSON已经成为API数据格式的事实标准,即使如此,有时候我们也希望支持其他数据格式。在这种情况下,首先考虑的是客户端如何指定需要获取的数据格式。例如当客户端需要获取XML格式的数据时,该通过怎样的方式向服务器传达这一信息呢?一般有以下这些常用的方法。
- 使用查询参数的方法
- 使用扩展名的方法
- 使用在请求首部指定媒体类型的方法
在第1个使用查询参数的方法里,如下所示,通过查询参数指明所需要的数据格式
http://api.example.com/v1/users?format=xml
在第2个使用扩展名的方法中,就如果指明文件的扩展名那样,在URI的最后添加上.json或.xml来指明所需要的数据格式
http://api.example.com/v1/users.json
第3种方法就是使用名为Accept的请求首部来指明所需的数据格式。例如
GET /v1/users
HOST:api.example.com
Accept:application/json
至于应该使用那种方法,可以说是个人偏好的问题。目前的话推荐使用第1种,因为通过URI指定的方法更加方便,对除初学者也更加友好,同时使用查询参数的话可以使用默认形式,方便理解。
尽可能地减少API访问次数
在决定通过API返回的响应数据格式时,首先要考虑的是如何尽可能地减少API访问次数。
下面我们举一个例子:以获取社交网友好友列表为例,如果该API返回的结果如下所示,会如何呢?
{
"friends": [
1232,
4564,
7289
]
}
这个接口只是把好友的ID返回,这样做会使得返回结果的数据量很小,同时我们也能猜到客户端拿到这些数据下一步要做什么,肯定会去获取好友信息,那么客户端就要根据这些ID再去请求一次API来获得各个用户的信息,那么这时候我们就可以修改API,在返回ID的时候,也能返回其他的信息。
API访问次数的增加不仅会为用户添加困扰,还会增加HTTP的负载,从而降低应用程序的速度,甚至还有可能加大服务器的负担,百害而无一利。
让用户来选择响应的内容
可以想到的最简单的方法就是让所有的API尽可能的返回多的数据,例如前面返回用户信息的例子。
这样做的话确实达到了无需访问多次API的目的,但却让客户端必须去接受远远超过需求的大量数据。
我们大家也知道在通信过程中发送,接收的数据量越小越好,应极力避免在不需要所有信息的时候发送过大的数据。
即使让每个API都返回适当的结果,如果用例复杂多样的话,要确定什么样的返回结果是适合的也非常困难。服务范围较大的API的情况下,如果只从方便API提供者的角度来决定如何设计,就会导致API高不成低不就, 最终让所有用户都觉得难用。
这时常用的方法就是使用户能够选择要获取的项目,比如如果用户想获取姓名和年龄,就可以在API调用时通过查询参数来指定,如下所示:
http://api.example.com/v1/users/12345?fields=name,age
封装数据结构
封装的意思就是表示用统一的结构将所有数据(包括请求数据和请求数据)包装起来,如下所示:
{
"code": 0,
"msg": "",
"data": {
}
}
观察一下数据结构就会发现,我们用code来表示状态码,msg表示状态码对应的信息,data里面就是返回客户端那边实际有效的数据。
封装所带来的便捷性显而易见,在通过API返回响应数据时,如果数据格式统一,在设计客户端时就更容易进行抽象化处理。
出错信息的展示
在使用API时很有可能会因为各种情况而导致返回出错信息,比如指定了错误的参数或访问不被允许时,都必须返回相关的出错信息。另外,当服务器处于维护状态或因为某种原因而停止运行时,也必须将其作为出错信息传达给客户端。
在错误发生时,如果只返回“现在发生了错误”,但又不告诉你具体是什么错误,那肯定是不友好的,这种情况下客户端不知道该采取什么样的措施来解决,因此必须向客户端返回尽可能多的信息。
在返回出错信息之前,首先必须选择合适的状态码,很多开发会去利用HTTP 的状态码,例如200,404,403等状态码。
实际上目前更推荐自定义状态码方式,我们以支付宝的api为例,文档链接:https://opendocs.alipay.com/common/02km9f
什么情况下需要更改API?
目前的话,有三种更改API的情况:
Web API承担着应用程序接口的角色,应用发布后,往往难以保持一成不变。随着时间的推移,应用会根据各种情况不停地发生变更。如果在线服务只是外观上发生少许变化,或者变化只对内容有影响但没有影响数据格式,那么也就没有必要去更新API了,但如果是数据格式发生了变化,或者需要在信息检索时添加新的查询参数等,就必须对API进行相应的变更。
- 公开发布的API
- 面向移动应用的API
- Web服务器使用的API
1:公开发布的API
像微信,支付宝这些公开发布的API,如果这类API的设计突然发生变更,结果会如何呢?
使用这些API的在线服务可能会突然无法理解端点内容,导致输出错误并停止运行。或者服务没有停止,但由于数据格式不是原先预设的样子,因此也很有可能会出现页面显示异常等问题,如此一来,API的用户就不得不去修改相应的业务逻辑。
如果更改API可以让API变得更加易于使用,能够给用户带来很大便捷,那么也允许进行大幅度更改,但不管如何,毫无征兆地更改接口一定会带来很多问题,让用户配合你更改API,在你完成API的更改后及时更新在线服务,这样的想法未免太过自私和任性。
另外,将API变更的信息告知所有用户本身就是一件很难的事情,虽然我们能够通过文档或Web站点发布通知来告知用户,但很难知道究竟有多少用户可以看到,就算看到了,用户也有可能因为繁忙而无法及时处理。
如果在无法保证用户能很好应对的情况下强行变更API,就会导致使用该API的在线服务发生各种问题,让用户觉得你所提供的API非常不可靠,从而导致用户流失。因为谁都不愿意使用那些会突然变更规范,让人不得不去紧急应对的API。
2:面向移动应用的API
因为只有你公开发布的应用在使用它,所以当API发生变更时,也只有你的应用需要进行相应的变更,也就是说,变更API影响的范围会非常小,即使如此,也不代表着我们可以随意更改API,因为移动客户端应用的更新完全取决于用户,如果用户不主动更新,那么移动客户端应用就始终是旧版本。
另外,更新客户端本身就非常花时间。新的接口要部署到正式环境,要测试一遍,测试通过后,APP要提交到市场,然后审核,接着上架。这些步骤都需要时间。另外,即使上架成功,我们也无法保证所有的用户都会第一时间去升级。
3:Web服务器中使用的API
如果是在自己的在线服务中使用的API,那么情况会好一些。因为客户端的代码只是和自己的服务器进行通信,同时更新二者并不困难,只是这里遗留了浏览器缓存的问题。因为API返回的数据和对其解析并处理的客户端都有被缓存的功能,如果其中一个发生了更新,而另一个是老版本,就可能因为数据不一致而引起系统异常。
通过版本信息来管理API
上面我们提到了3种可能更改API的情况,并了解了API的突然变更会导致的问题,总之,一旦对已公开发布API的设计规范进行变更,就一定会有引发异常的危险,那么我们如何避免呢?
最容易想到的方法就是尽量不去修改已公开发布的API,但这样就很难进行在线服务的改善工作,所以这一方法是行不通的。新API只需通过新的访问方式公开即可,比如使用不同的端点,或者使用添加了其他参数的URI等。
也就是说,对于旧方式访问的客户端,和之前一样发送数据即可。而对于使用新方式访问的客户端,则需要返回更新后的数据,也就是说我们可以提供多个版本的API,如图所示:
让新旧两个版本以上的API共存的方式,目前的话有以下三种:
- 在URI中嵌入版本编号
- 在查询字符串中加入版本信息
- 通过媒体类型来指定版本信息
1:在URI中嵌入版本编号
首先我们看一个在URI中嵌入版本编号的范围
http://api.example.com/v2/user/123456
该API的路径中有一个v2,这便是API的版本编号,还有的人习惯用下面这种
http://api.example.com/2/user/123456
那么那种更好呢?我更推荐使用v的方式,因为v表示version,版本的意思,使用这个可以让人一目了然,易于理解。
2:在查询字符串加入版本信息
除了在URI路径中指定API版本信息外,另外一种常见的方法就是在URI中使用查询字符串来指定。例如:
http://api.example.com/user/123456?v=2
使用路径的方式和使用查询字符串的方式最大的不同在于,在使用查询字符串指定API版本时,该部分内容可以省略。在这种情况下,当客户端访问该类型的API时,服务器端往往会直接使用默认的版本,大多数情况下,服务器所使用的版本就是最新版本。这时如果用户在访问API时没有添加版本信息,就可能会因为版本的突然更新而导致程序有问题。
3:通过媒体类型来指定版本信息
我们也可以通过媒体类型来指定API的版本信息。媒体类型用来表示数据(文本)的格式,HTTP协议里用Content-Type首部来制定,例如JSON的媒体类型是application/json,XML的是application/xml。
某些基于JSON或XML格式定义的新数据类型也可以指定媒体类型,比如RSS可以指定为application/rss+xml,在子类型之后加上xml,表示新的数据类型源自xml。例如GitHub版本3的API就指定了其返回的数据媒体类型为application/vnd.github.v3+json,一看该媒体类型,就能明白它是Github版本3的API的数据格式,并且描述方式来自JSON。
使用媒体类型指定API版本的情况下,客户端向服务器端发送请求消息时,必须在Accept首部里嵌入含有API版本的媒体类型。
Accept:application/vnd.exaple.v2+json
而服务器端则会根据客户端所需的媒体类型生成相应的响应消息并返回,在返回响应消息的同时,服务器端还会附加Content-Type和Vary首部。
Content-Type:application/vnd.example.v2+json
Vary:Accept
应该采用什么方法?
在前面的3种方法中,并没有说哪种是最优秀的,但最为常用的还是在URL的路径中嵌入版本信息,并遵循版本控制规范,使用主版本编号,这一方法可以让人仅从URI中就可以看到API的版本,易于理解和接收,如果没有特殊的情况,采用这一方法是最为保险的。
应对大规模访问的对策
为了解决突然出现大规模访问的问题,最现实的方法就是对每个用户的访问次数做出限制,也就是确定每个用户在单位时间里的最大访问次数,如果用户已超过最大访问次数,那么当用户再次访问时,服务器端将直接拒绝访问并返回出错信息。比如设置单个用户在1分钟内只允许进行60次访问,那么当用户在1分钟内发起第61次请求时,服务器端便会返回错误信息,而1分钟后用户又能继续访问。
如果要实施访问限速,就要先解决如下问题:
- 用什么样的机制来识别用户
- 如何确定限速的数值
- 以什么时间单位来设置限速的数值
- 在什么时候重置限速的数值
一般情况下可以使用用户ID或者IP地址来识别用户。
设置访问限速的初衷是为了避免服务器端在短时间内遭遇大规模访问而不堪重负导致无法继续提供服务的情形,而如果访问限速让正常使用API的用户感到不便,也就失去了对外公开API 的意义,因此要尽可能地了解你所提供的API会在怎样的情况下被用户使用,并以此为根据来设置访问速度。
接下来我们讨论下限速的时间单位,是天,小时,还是分钟呢?建议就是当前的业务需求以及客户实际操作场景来设置限速时间单位。
另外,我们还需要考虑是对所有端点设置统一的访问次数上限,还是对每个端点都分别设置访问次数上限,建议将API分为若干组,并分别为每个组设置访问次数上限。
至于在什么时间重置限速的数值,一般就是当第一次超过最大访问次数的时候记录下时间,等过了设置好的时间单位后就可以重置限速的数值了。
参考书籍
《Web API的设计与开发》