前言
没有最优的方案,只有最适合的方案,本文指出对APP接口设计的一些规范与大家分享和共勉。
涉及到APP接口设计规范,设计案例的分享,和一些PHP编码的要求,目的在于开发出性能优异,结构清晰,维护便捷,安全,和高拓展性的接口。抛砖引玉
经验学习
在项目中,要做到融会贯通,首先就应该做到多学习,学习大厂的经验和总结,如果能避免到别家遇到的坑,那么就最好了,如果避免不到,那么也能做到心中运筹帷幄。程序开发中有句很流行话就是,”不要重复造轮子”,要”时刻明确自己是搬砖民工,不是烧砖的窑”。
一、案例分析
新浪微博 open api
获取用户详情接口设计 http://open.weibo.com/wiki/2/friendships/friends
{
"users": [
{
"id": 1404376560,
"screen_name": "zaku",
"name": "zaku",
"province": "11",
"city": "5",
"location": "北京 朝阳区",
"description": "人生五十年,乃如梦如幻;有生斯有死,壮士复何憾。",
"url": "http://blog.sina.com.cn/zaku",
"profile_image_url": "http://tp1.sinaimg.cn/1404376560/50/0/1",
"domain": "zaku",
"gender": "m",
"followers_count": 1204,
"friends_count": 447,
"statuses_count": 2908,
"favourites_count": 0,
"created_at": "Fri Aug 28 00:00:00 +0800 2009",
"following": false,
"allow_all_act_msg": false,
"remark": "",
"geo_enabled": true,
"verified": false,
"status": {
"created_at": "Tue May 24 18:04:53 +0800 2011",
"id": 11142488790,
"text": "我的相机到了。",
"source": "新浪微博",
"favorited": false,
"truncated": false,
"in_reply_to_status_id": "",
"in_reply_to_user_id": "",
"in_reply_to_screen_name": "",
"geo": null,
"mid": "5610221544300749636",
"annotations": [],
"reposts_count": 5,
"comments_count": 8
},
"allow_all_comment": true,
"avatar_large": "http://tp1.sinaimg.cn/1404376560/180/0/1",
"verified_reason": "",
"follow_me": false,
"online_status": 0,
"bi_followers_count": 215
},
...
],
"next_cursor": 5,
"previous_cursor": 0,
"total_number": 668
}
面向对象设计:用户是一个完整的对象,其中每个用户包含一个最新微博的对象,微博对象也是一个相对完整的对象,按照新浪微博的APP接口设计,获取每一个用户列表,都能获取到列表用户的最新微博信息。
错误信息的返回:主接口中没有直接对错误信息的定义,但凡是能够获取到信息,都认为是正确,而错误信息的定义则是用另一种数据格式来表示。
{
"request" : "/statuses/home_timeline.json",
"error_code" : "20502",
"error" : "Need you follow uid."
}
错误信息的说明:’request’表示当前请求的接口;’error_code’表示错误编号;’error’表示错误的提示文字
淘宝开放平台
查询买家信息 http://open.taobao.com/docs/api.htm?spm=a219a.7629065.0.0.5Zpljz&apiId=21348
正常响应:
{ "user_buyer_get_response":{
"user":{
"nick":"hz0799",
"sex":"m",
"avatar":"http://assets.taobaocdn.com/app/sns/img/default/avatar-120.png",
"open_uid":"324324324"
} } }
异常响应
{
"error_response":{
"sub_msg":"非法参数",
"code":50,
"sub_code":"isv.invalid-parameter",
"msg":"Remote service error" }
}
错误响应也是通过错误code来识别的,每个code对应一个错误内容
其他开放API
除了上文说的响应方式外,还有一种API响应方式也是比较流行,代表者是百度,高德,支付宝等。
- 百度举例
{
address: "CN|北京|北京|None|CHINANET|1|None", #详细地址信息
content: #结构信息
{
address: "北京市", #简要地址信息
address_detail: #结构化地址信息
{
city: "北京市", #城市
city_code: 131, #百度城市代码
district: "", #区县
province: "北京市", #省份
street: "", #街道
street_number: "" #门牌号
},
point: #当前城市中心点
{
x: "116.39564504", #当前城市中心点经度
y: "39.92998578" #当前城市中心点纬度
}
},
status: 0 #结果状态返回码
}
支付宝举例
{
"alipay_trade_precreate_response": {
"code": "10000",
"msg": "Success",
"out_trade_no": "6823789339978248",
"qr_code": "https://qr.alipay.com/bavh4wjlxf12tper3a"
},
"sign": "ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
}
这类API相对新兴设计,在API响应上,不区分正确响应和错误响应的结构,采用统一返回方式,其中返回的code 来区分正确还是错误,其中的message来分别给出正确和错误的响应消息。
我们自己的API响应规则
选型我们自己使用的API相应规则如下
正常响应示例
{
"status": 1,//状态值是1
"error_code": 0,//同时错误代码是0表示无错误
"message": "获取成功",//提示操作成功信息
"data": []//主体返回内容
}
错误响应示例
{
"status": 0,
"error": 20102,//2:业务级错误(1代码系统级错误)固定一个字符;01:指的是01这个业务比如保洁 固定 两个字符;02具体错误信息固定两个字符,会整理成一个对照表格,前端需要翻译 成友好的提示
"message": "用户id不能为空",//给前端程序员的不友好提示,指明错误的原因
"data": {} //data字段固定
}
小结
API设计中,要根据自己的业务类型和面向群体来综合考虑
都对错误级别进行了划分,并用不同的错误code来表示
API面向对象设计并不是面向页面设计,这样的API就具有了多端不同展示的能力
响应主体设计中,个人比较倾向统一的返回,也就是现在我们正在使用的三段式返回,不同的客户端不用去写过多的兼容代码和错误处理,错误处理全部由服务端来完成。
错误处理,目前我们的错误处理机制都还比较简单,就APP而言,只有 ‘0’ ‘1’ ‘1001’,三种识别码和对应的中文提示内容。如果我们后续要支持多种语言的提示,就必须使用error_code,然后由客户端自行提示。
二、接口设计规范
与前端交互部分
这里概念的定义为与APP部分相互的部分,我们将从安全,版本兼容,命名规范,迭代,面向对象等多个方面说起
接口安全部分
在app的后端设计中,一个很重要的因素是考虑通讯的安全性。
避免信息的泄露,最简单的方案是所有涉及到安全性的api请求,都必须要使用https协议。
因此,我们需要考虑的要点有:
1. 在app和后台,都不能保存任何用户密码的明文
2. 在app和后台通讯的过程中,怎么保证用户信息的安全性
3. 在app中,根据安全考虑,用户的操作分为两类:
4. 用户登录
5. 注册操作
6. 用户的其他操作
在第一点,用户登录注册操作中,是会出现用户密码,所以在这个过程中,必须要使用https通讯,保证通讯的安全。
在第二点,用户的其他操作,怎么保证这部分通讯的安全呢?
在我们的设计中,是采用了公钥加私钥保证安全。用户的id是公钥,通过一定的算法对用户的id进行加密得到一个加密字符串是私钥。当用户登录或注册后,通过https把公钥加私钥返回给app客户端。
但这个方法有个缺点,当别人截获了这个url时可以重复使用,所以有个改进方法是在传递的参数中增加时间戳,当发现这个时间戳离现在的时间已经很久了,就判断这个url已经失效了。但用时间戳怎么保证app的时间和服务器的时间同步?
可以在app每次启动和注册登录时和服务器同步时间,然后在app内建一个时钟,时间戳在这个app的内部时钟获取,防止用户修改了手机的时间。
当然,这些操作做完了,也不能保证100%的安全,只能为攻击增加成本。
另外,现在越来越多App取消了密码登录,而采用手机号+短信验证码的登录方式,我在当前的项目中也采用了这种登录方式。这种登录方式有几种好处:
不需要注册,不需要修改密码,也不需要因为忘记密码而重置密码的操作了;
用户不再需要记住密码了,也不怕密码泄露的问题了;
相对于密码登录其安全性明显提高了。
效率
APP对服务器端要求是比较严格的,在移动端有限的带宽条件下或者弱网络下,要求接口响应速度要快,,抛开后端的开发框架效率来说,对数据要求也比较严格,如果能做到app需要什么数据就传什么数据,不可多传,过多的数据量影响处理速度,最重要的是影响传输效率那么自然效率是最高的,但是这之间也要有个取舍,效率和接口设计思想之间,后面我们会提到面向对象的设计思想。
面向对象的设计思想
Restful风格:RESTfu设计原则,它被RoyFelding提出(在他的”基于网络的软件架构”论文中第五章)。而REST的核心原则是将你的API拆分为逻辑上的资源。这些资源通过http被操作(GET,POST,PUT,DELETE)。但现在看,一般的操作只有两种:GET ,POST。
这个设计原则最简单的应用就是面向对象设计而不是页面来设计api。最开始的时候,app的一个页面需要什么数据,api就返回什么数据。结果随着app的UI不断改版,需要的数据不断变化,不停地修改api,最后当api的改动会影响以前的版本的时候,只能写一个新的api版本,最后弄得api中有很多version/2,version/3这样的标志,恶梦!
但根据object来设计,又有一个问题,一个大object可能包含很多小object,是一个api返回全部小object,还是分为多个api返回?根据业务和技术,带宽等仔细考虑吧。
目前我们的接口设计是根据业务来定制接口和返回,假设页面上只显示五个字段,那么后端就需要针对这个页面进行设计。
当然这这样的好处是显而易见的,在和客户端交互的过程中,传输的数据全是有用的数据,极大地节约了网络资源,而且只需请求一个接口,接口就返回了所有界面显示的数据,在弱网状态下,加快响应速度。
新浪微博的做法:打开个人中心,会分多次进行请求,’users/counts’批量获取用户的粉丝数、关注数、微博数,’users/domain_show’个性域名相关,’users/show’获取主要信息,这是比较极端的做法,仅供参考。
下面是一个简单的例子
返回的数据结构如下
{
"brand_name": "奥迪",
"car_model": "SUV",
"emission_standard": "国五",
"car_owner": {
"name ": "张三 ",
"driving_years": "5年",
"id": "666"
}
}
其中车主是一个对象,车子是一个对象,两者是既有关系又相对独立。
总结建议是,新设计的接口,需要考虑到多端不同展示,尽可能的偏向于面向对象,少量特殊处理可以面向页面。当然,后端代码上,都是以对象的形式存在,逻辑必须清楚。
API命名规则
其中一个原则,一看api名字就知道这个api是干啥。但是有个问题就是当你要负责几十甚至上百个api,你就知道不能”望名知api”是个什么样的痛苦。
就拿一个接口来举例吧
'/User/userRedDot/version/1'
这是我们在使用的一个接口,从接口名字来看,不难看出User这个是用户相关的一个功能,然后userRedDot小驼峰命名指的是用户小红点,然后接口的版本号是第一版。以上4部分构成了一个完整的接口命名。
传参规则
接口文档中是会注明不同的接口该使用不同的传参方式
header参数部分:请求头部一般放入鉴权的相关参数,比如用户的token和签名,设备id,APP的标识,userAgent自定义等。
除去鉴权的参数,其他就是接口的入参传递(文档会注明传递方式):
1. 一维参数 按照POST/GET按照普通的form-data和urlencode方式即可
2. 多维参数 按照POST方式,并把body放入json的形式传递。
3. 新增数据 POST
4. 获取数据和修改数据用GET
接口使用规则
系统级接口需要独立于业务之外使用,对于系统级接口,需遵循接口使用规则,对于非系统级接口,可由具体业务实现。但是安卓和ios需要统一。
打个比方,获取用户权限的接口,接口的使用规则由产品给出的,切换tab和从后台返回。
再比如,获取编辑用户爱好,婚姻情况,个性签名等筛选数据,按照接口使用规则,应该在进入用户中心点设置的时候请求,现在是在打开APP地方的进行请求,不符合接口使用规则。原因也在于以前没有给出接口使用规则。
特殊处理的接口需要在接口文档上注明使用规则,比如接口的先后顺序,接口的使用环境和频率。
对于接口返回,也需要安卓和ios进行同步处理,操作成功和 操作失败的信息提示,成功是都不处理还是都处理,失败的提示信息怎么处理,操作失败的时候提示信息需要明确。两个客户端不应该存在不同的处理方案
兼容性原则
接口不可能永远不变,它会随着需求的变化而做出相应的变动,这种变动也可以理解为兼容或者不兼容。大部分情况下直接在这个接口上叠加版本号,并兼容旧版本。App的新版本开发传参时则将传入新版本的version。
接口的变化一般会有几种:
1. 数据的变化,比如增加了旧版本不支持的数据类型(兼容:新增版本号,接口增量更新)
2. 参数的变化,比如新增了参数(兼容:新增版本号,接口增量更新)
3. 接口的废弃,不再使用该接口了(不兼容:原接口指定版本废弃,后端逻辑处理;原接口所有版本废弃,如果是业务流程修改,则停用原接口,并新开接口)
4. 如果整个接口系统的根基都发生变动的话,比如微博API,从OAuth1.0升级到OAuth2.0,整个API都进行了升级,就无法兼容,只能进行版本强制升级了。
有时候,一个接口的变动还会影响到其他接口,但做的时候不一定能发现。
服务端异常处理
服务端的程序在运行的时候,可能因为一个数据的转化或空指针异常什么的,都不能让程序奔溃,需要捕获异常并对异常进行处理,并返回明确的数据状态信息,不管是成功的,还是失败的,都必须要有数据返回给APP客户端,否则,接口的协议失去了所有的意义
app客户端的语言 java和object-c都是强类型语言,所以怎么处理空值显得特别重要,不合理的设计很容易造成app的闪退。
从后台的角度来说,api中返回的数据中,正确值和空值的类型必须一样,举例,用户名的字段是“realname”: “xxx”,如果用户名为空,则应该返回“realname”:”“。如果返回值是一个array,空数据则返回一个空array,如果返回值是一个对象,空数据则返回一个空对象,绝对禁止null值。
对于客户端,必须用个全局的函数来处理所有api的返回数据,需要有一个机制:对于某个客户端需要数据,如果api中缺失,客户端自动补上并给予默认值。
同时,在数据库设计的时候,一个合理的设计必须是所有字段都有默认值,不应该允许null值。null在大量的语言和数据库中,会带来无穷的问题。
如果服务端是php,还有一个问题,php中数组和字典都是array,但是可以用(object)[]返回对象,但在java和object-c中是不一样,这个问题一定要注意。
数据格式
补充说明下json的六种数据类型数据类型和约定
Number:整数或浮点数
String:字符串
Boolean:true 或 false
Array:数组包含在方括号[]中
Object:对象包含在大括号{}中
Null:空类型
前后端需要对数据类型进行约定:
- 时间日期型数据:直接返回格式化后的时间字符串或者直接返回时间戳
- 数字类型和文本类型:统一使用字符串格式
- 布尔值类型:统一使用字符串’0’和’1’来表示假和真
- 不返回Null类型数据
接口版本的设计
接口不可能一成不变,在不停迭代中,总会发生变化。接口的变化一般会有几种:
数据的变化,比如增加了旧版本不支持的数据类型
参数的变化,比如新增了参数
接口的废弃,不再使用该接口了
为了适应这些变化,必须得做接口版本的设计。实现上,一般有两种做法:
1. 每个接口有各自的版本,一般为接口添加个version的参数。
2. 整个接口系统有统一的版本,一般在URL中添加版本号,比如http://api.domain.com/v2。
大部分情况下会采用第一种方式,当某一个接口有变动时,在这个接口上叠加版本号,并兼容旧版本。App的新版本开发传参时则将传入新版本的version。
如果整个接口系统的根基都发生变动的话,比如微博API,从OAuth1.0升级到OAuth2.0,整个API都进行了升级。
有时候,一个接口的变动还会影响到其他接口,但做的时候不一定能发现。因此,最好还要有一套完善的测试机制保证每次接口变更都能测试到所有相关层面。
APP后端代码部分
面向对象设计,必须具有高拓展性来应对各种需求的变更
抽象的颗粒度的把握,颗粒度必须很细,但是又不能太细了,但是实体对象必须是独立的。
代码层次结构必须清晰,明确每一层干什么事情,做到适当的解耦,不依赖用不到的方法和函数
面向接口设计,多人分工合作中,提供的代码的最小单位是接口,使用者无需关注接口的内部实现,提供接口的人后期维护该接口。
代码重合部分,不难发现,目前系统中有很多功能类似的代码,但是又发现这些代码都有用处,维护这些代码就非常痛苦,当要改一个基础的数据的时候,就会去改很多
SQL部分,优化SQL查询,提升查询效率,杜绝使用join和写复杂sql等不便于维护的代码,不用jion和复杂sql之后能对系统的扩展和二次开发有帮助,提高系统和数据库的并发[目前能知道的是新浪微博已经全面禁止使用join查询了]。
待商榷问题
Herder管理:对herder的规划和规范,包括设计和命名上,还有技术层面上的难题,尤其注意原生里面的内嵌H5页面
push到达率:目前的push能否满足现状的需求,如果不满足,那么需要怎么样才满足。是否需要引进新的第三方push
客户端更新和热更新:能否后续的功能用react来编写,集成热更新的能力,原生更新的逻辑梳理,和是否合理,不合理该如何调整。
权限和用户权限:梳理现在的APP权限和用户权限设计模式和交互模式,看是否合理,不合理的话,怎么调整。
接口依赖关系:接口之间原则上的依赖关系不能超过两个,意思是一个接口需求的数据,最多只能从一个接口处获取,如果情况特殊另说。