前言
对于前后端分离的项目,主要是以API为界限进行解耦,那么在web开发中,对于api的设计能够遵循REST设计原则,即可以称为Restful api。使用Restful设计api主要有两点好处,一是表现力更强,更易于理解;二是Restful为无状态的,不管前端是何种设备何种状态都能够无差别的请求资源。
构建步骤
一:基于业务领域的数据建模,而不是基于功能建模
基于功能建模时,会造成api臃肿并且出现api难以理解的情况、学习成本的上升,同时会给编码维护人员造成相当大的困难,所以在设计api时如果做好统一的规范,基于业务数据来建模,编码人员有一个统一的共识,不仅方便代码的编写,而且对于api的维护也相当方便。
基于业务领域数据建模时,标准的HTTP方法已经提供了一套约定俗成的操作语义,如查询所有书籍
GET /api/books
复制代码
查询某一本特定书籍
GET /api/books/{id}
复制代码
新增一本书籍
POST /api/books
复制代码
修改一本书籍
PUT /api/books/{id}
复制代码
删除一本书籍
DELETE /api/books/{id}
复制代码
而基于功能建模的api的设计可能如下所示
/api/getAllBooks
/api/getBookById
/api/saveBook
/api/updateBookById
/api/deleteBookById
复制代码
二:设计URL的表现形式
两大原则:规范的(regular)、可预测的(predictable)
1. 只使用名词
2. 要有切入点
3. 要选择合适的id形式
4. 要表达资源间的联系
5. 要支持返回部分资源
使用名词: 在URL中使用名词,代表着基于业务领域的对数据的操作,使用动词则意味着你基于功能的建模。
要有切入点: 所有的URL都要有一个根路径,所有的对数据的操作的URL都是从这个根路径延伸出去的。
要选用合适的id形式: 在api中的路径中包含id的形式,这个取决于后端的数据库,对于像Mysql这样的主键自增功能,可以直接使用整数值,对于Mongodb这样的key-value类型的数据库,可以使用字符串,但是这里要确保如何去处理全局的唯一字符串问题。
要表达资源之间的联系: 对于资源之间存在联系的,如父子关联,要使用合适的url去形象的表达。比如属科幻类的书籍。用GET /bookType/1765328/books,这种模式可以简括为
/{relationship-name}[/{resource-id}]/.../{relationship-name}[/{resource-id}]
复制代码
要支持返回部分资源:
/books?fields=name,author,publishing
复制代码
三:设计数据的表现形式
- 添加self link
- 集合数据展现
- 数据分页
添加self link: self link提供了一个上下文环境,客户端可以更容易理解当前的resource的位置。
{
"book": {
"name": "三体",
"author": "刘慈欣",
"url": "https://api.book.com/books/1756278"
}
}
复制代码
集合数据展现
{
"books": [
{
"name": "三体",
"author": "刘慈欣",
"url": "https://api.book.com/books/1756278"
},
{
"name": "平凡的世界",
"author": "路遥",
"url": "https://api.book.com/books/1756238"
}
]
}
复制代码
数据分页: 当服务器返回的数据很大时,不能一次全部返回,分批返回数据是最合适的。
GET https://api.book.com/books?limit=20,offset=0
结果集
{
"self": "https://api.book.com/books?limit=20,offset=0",
"kind": "Page",
"pageOf": "https://api.book.com/books",
"next": "https://api.book.com/books?limit=20,offset=20",
"contents": "...."
}
复制代码
在查询的集合中,使用pageOf来表明查询的起点,next指向下一页,如果有上一页,则previous来指向上一页。
四:错误处理
后台异常要统一进行处理,一般分为业务异常以及非业务异常,业务异常一般指参数校验不通过、权限校验失败等异常。非业务异常表示不在预期内的问题,通常都是由于自己代码的逻辑错误所导致的。在controller层使用统一的异常拦截处理器进行拦截处理。异常描述通常如下所示
业务类异常
1:抛出该类异常,并设置相应的状态码
2:异常的文本描述
常用状态码
400:常用在参数校验
401:未登录
403:未授权
404:资源不存在
500:服务器出现错误
五:多版本控制
当项目改动比较频繁,经常发生版本的迭代升级时,会有使用多版本的需求,但是对于多版本确是一个比较有争议的话题,比较不好统一处理,需要针对不同的版本做不同的针对性处理,是一个很麻烦的事情,会增加测试以及部署的成本,效益甚微。主要有两种方式来解决多版本问题
1:不显示支持,通过对api做一些操作,保持api服务的兼容性,而不是通过提供多个api版本来让用户使用。
2:使用Http Accept Header,在头部中添加version信息,不要在url中使用版本信息,这样在api进行更换时,就不用全部去修改一遍代码了。
注意事项
一:跨平台性
跨平台指我们的接口要支持不同的终端设备,比如android、ios及桌面软件等,采用通用的解决方案,通信协议就采用最常用的HTTP协议,即时通信采用开放的XMPP协议,做游戏采用可靠的TCP协议,如果TCP不能够满足需求,可采用定制的UDP协议。数据交换采用xml或者json或者wenservice。
二:良好的响应速度
接口应该以最快的速度将数据返回给请求者,目标就是要快,一个页面,秒开最好,超过三秒就需要找找原因了。数据量按需分配,客户端需要什么数据就返回什么数据,过多的数据量会影响处理速度,最重要的是影响传输效率。
三:接口要为移动端考虑
针对不同的操作习惯,web端和移动端可能要分别针对性设计,比如在移动端,下拉刷新和上拉加载,而对于web端可能更多的是按页读取,这个时候就得分开设计,提供额外支持。
四:考虑移动端的网络情况和耗电量
对于移动端,因为设备,网络等情况比较复杂,在可以的情况下,我们可以获取客户端的网络情况,设备信息等,然后做相应不同的处理,比如在wifi情况下,才给用户展示图片,无wifi情况下,增加善意提醒。
五:通用的数据交换格式
目前对于客户端和接口的数据交换格式,通常就三种,xml/webservice/json,大部分使用json,使用json比较麻烦的两点就是要处理Date类型和NULL,json本身没有Date类型,Json库将Date类型的数据序列化时会转为String,转换会依据不同环境、不同的环境以及不同的Json解析库而有所不同,所以转换后的结果经常会有所不同,比如你在客户端结果是"2018-09-24 16:38:00",到服务器后结果为"Sep 24,2018 4:38:00 PM",客户端进行反序列化时无疑会失败。解决的办法:一是Date类型统一采取时间戳表示,二是直接使用时间字符串。另外一个麻烦就是json对于NULL,会把它转换为string的"null",解决办法一是尽量减少null的使用,二是对null做判断进行特殊处理。
六:接口统计功能
在做PC端网站的时候,我们都会给我们的网站加上个统计功能,要么自己写统计系统,要么使用第三方的比如GA。移动端接口API则需要我们自己实现统计功能,这时就需要我们尽可能多的收集客户端的信息,除了传统的IP、User-Agent之外,还应该收集一些移动相关的信息,比如手机操作系统,是android还是ios,都是什么版本,用户使用的网络状况,是2G、3G、4G还是WIFI。客户端APP是什么版本信息。
七:客户端与服务端的平衡
在移动开发中,由于客户端的修改会很费时费力,特别是IOS应用还要经过Apple审核,另外,当前IOS开发人员、Android开发人员的人工成本普遍较高,人才紧缺,基于这两点,能在服务器端实现的功能就不要放在客户端,毕竟服务器端程序的修改要比客户端方便、灵活、快捷的多。
八:隐式用户与显式用户
显式用户指的是,APP程序中有用户系统,一个username、password正确的合法用户,称之为显式的用户。通常显式用户都需要注册,登录以后能完成一些个人相关的操作。 隐式用户指的是,APP程序本身就没有用户系统,或者一个在没有登录的情况下,使用我们APP的用户。在这种情况下,可以通过客户端生成的UDID来标识一个用户。有了用户信息,我们就能够了解不同用户的使用习惯,而不仅仅是全体用户的一个整体的统计信息,有了这些个体的信息之后,就可以做一些用户分群、个性化推荐之类的事情。
九:安全问题
设计API第一个需要考虑的是API的安全机制,制定API的安全机制,主要就是为了解决这两个问题
保证API的调用者是经过自己授权的App: 采用设计签名的方式。对每个客户端分别分配一个AppKey和AppSecret。需要调用API时,将AppKey加入请求参数列表,并将AppSecret和所有参数一起,根据某种签名算法生成一个签名字符串,然后调用API时把该签名字符串也一起带上。服务端收到请求之后,根据请求中的AppKey查询相应的AppSecret,按照同样的签名算法,也生成一个签名字符串,当服务端生成的签名和请求带过来的签名一致的时候,那就表示这个请求的调用者是经过自己授权的,证明这个请求是安全的。而且,每个端都有一个Key,也方便不同端的标识和统计。为了防止AppSecret被别人获取,这个AppSecret一般写死在代码里面。另外,签名算法也需要有一定的复杂度,不能轻易被别人破解,最好是采用自己规 定的一套签名算法,而不是采用外部公开的签名算法。另外,在参数列表中再加入一个时间戳,还可以防止部分重放攻击。
保证数据传输的安全: 采用HTTPS了。HTTPS因为添加了SSL安全协议,自动对请求数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,主要就是防止中间人攻击。因此,为了安全考虑,建议对SSL证书进行强校验,包括签名CA是否合法、域名是否匹配、是不是自签名证书、证书是否过期等。