前言
虽然一个api
接口的业务,数据逻辑是后端提供的,但真正使用这个接口的是客户端,一个前端功能的实现流程与逻辑,客户端算是接口的需求方。所以建议在前期接口设计和评审时,客户端的应该更多的思考和参与,什么时机调什么接口?每个接口需要哪些字段?数据含义怎么给?只有这些都考虑清楚,且达成一致并产出接口文档后,当项目真正启动时,根据接口协议进行开发,才能尽量避免各种不确定因素对项目整体进度的影响。
API
设计核心原则:
- 遵循
Web
标准 - 对开发者友好
- 在使用上应该保持简单、直观和一致性,不仅仅是容易使用,而应该让使用者感到愉悦
- 有效率的
接口设计规范
基本规范
1.通用请求参数
每个请求都要携带的参数,用于描述每个请求的基本信息,后端可以通过这些字段进行接口统计,或APP
终端设备的统计,可以放在请求头部header
中。
const header = {
version: '1.0.0', // 客户端版本version
token: '6aab9eb8f32e566dfa41cbd48f53c80d', // 登陆成功后,登陆令牌token
platform:'android', // android/ios
model:'Note3', // 机型信息
appid:'' // app唯一标识,服务可能提供多个app应用
}
总是给你的API
加上版本化。API
版本化能够让你的API
迭代地更快,防止老的请求访问你的API
最新版。这样能够让你在进行API
重大升级时,能够持续对老版本API
进行一段时间的支持。
有很多的人在讨论,API
的版本化,应该体现在URL
上,还是在请求Header
里。理论上讲,应该放在Header
里。
API
被发布出来,不会永远不变。变化是不可避免的。重要的是,我们怎么去管理这种变化。
2.请求路由链接
命名规范的基础上尽量保持良好的可读性,见名知意。
接口请求路由参考restful
规范。
REST
的核心思想是,把API
按照逻辑上的资源来进行区分。这些资源被HTTP
的不同方法(GET, POST, PUT, PATCH, DELETE
)进行请求,每个方法代表不同的含义。(GET
=查找,POST
=增加,PUT
=修改,DELETE
=删除)
REST
的伟大之处在于,采用了既有的HTTP methods
,在一个单独的链接下,实现了多样的功能操作。你不需要遵循任何的命名规范,仅仅需要一个干净和整洁的URL
结构。
3.响应数据
数据返回格式:
{
code:200, // 200:成功;非200:失败
msg:'成功', // 请求失败时的message
Desc: '', // 返回描述字符
data: { // 数据实体
name:'张三',
age:20
}
}
array
类型数据:
{
code: 200, // 200:成功;非200:失败
msg:'成功', // 请求失败时的message
Desc: '', // 返回描述字符
data: { // 数据实体
list:[{
name:'张三',
age:20
}]
}
}
判断code==200
的情况下继续解析data
,code!=200
时,根据业务提示文案放在Desc
字段,客户端直接显示就可以了。
array
类型的数据,即使只有1个list
字段,也要保证data
下是个完整的object
结构,这样就可以统一将data
层级下的数据当对象解析。
4.命名规范
统一命名:一般采用小驼峰法,无绝对标准。
避免冗余字段:每次在新增接口字段时,注意是否已经存在同一个含义的字段,保持命名一致。
注释清晰(重要):每个接口/字段都需要有详细的描述信息,很多时候接口体现业务逻辑,是团队中很重要的文档沉淀,同时,详细的接口文档,可以帮助新人快速熟悉业务。
5.返回字段类型讨论统一
需要和两端商讨关于数据类型的返回,特别式特殊的类型,比如布尔类型等,不同语言表示方法不同
6.上传/下载接口,根据md5
校验数据完整性
上传,下载文件/图片时,除了file
本身,还要携带该file
的md5
,在传输过程中可能丢失部分数据,导致文件损毁,所以需要通过md5
值进行完整性校验。
上传成功后将图片url
返回
7.避免浮点型计算
浮点型计算可能导致精度丢失,为了避免,可以缩小单位进行存储。
例:1.5元,后端会以150分存到数据库,1.5km会存成1500m。
同理,如果一个类似距离的字段,如果是展示用,则直接返回"1.5km",如果涉及到逻辑判断与计算(如:>1000m,执行逻辑A,>1500m,执行逻辑B),可以返回"1500,单位(m)",至少比传1.5来的方便。当然如果要计算浮点型也是可以的,需要用到BigDecimal,这么设计只是为了减少出错的可能性。
8.json数据保持良好结构
{
userId: 1
userName: '洒哥',
userAvatar: 'http://xxx',
orderId: 2,
orderType: 1
addressId: 5,
addressDtl: '萝岗'
}
json
的3类信息user,order,address
,全部堆在一起,字段多了以后,对于接口信息的读取很不直观;
客户端在定义model
的时候,会将全部字段定义在一个model
中,如果其他地方也有用到addressId,Name,Detail
等字段信息,则需要重新定义address
的model
,无法实现model
的复用。
{
user:{
id: 1
name: '洒哥',
avatar: 'http://xxx'
}
order:{
id: 2
type: 1
}
address:{
id: 5,
detail: '萝岗'
}
}
优化后user,order,address
字段在各自的对象内,一眼就可以看出这个接口有哪些类型的数据。还有点要注意,如果放在同一级别,id
字段就需要用userId,orderId,addressId
区分开,而现在根据不同结构体区分字段类型后,直接使用id
就可以了。
撰写接口文档
好的文档,和好的接口同样重要。接口文档需要被很容易地找到和访问。大部分开发者会在进行接口开发之前,检查并查看接口文档。如果这些接口文档是写在PDF文档里,或者需要登录才能查看,那将不仅仅是难于查找,还不利于搜索。
接口文档应该描述完整,并附上具体的例子。最好是,这些例子应该是真实可以访问的。
一旦你发布了一个API
,那意味着,在没有通知调用者的情况下,你有责任不去破坏该接口的已有功能。如果你在今后修改该接口,需要及时更新接口文档,并且在发布接口的更新之前,及时通知你的接口调用者。
瘦客户端
众所周知,客户端任何的修改都是需要发版的,特别是iOS
需要走AppStore
的审核流程。为了修一个bug
,仅仅改几行代码,而重新走一轮发版流程,并且旧版本得bug
一直都在,很是劳民伤财的。所以在接口设计的时候,也需要适当考虑这点,将业务重心交给服务端,客户端保持逻辑简单。服务端可以随时发版,客户端一个版本却只能发一次,有些团队一开始并没意识到这点,总觉后端就是重度业务逻辑的所在,管那么多前端的展示,字符串拼接逻辑干嘛,可当真正有问题(bug
或需求变更)需要发版的时候,客户端需要重新一轮新的版本得发版。
客户端尽量只负责展示逻辑,不处理业务逻辑
例如:客户端有个TextView
,服务端只给个status
字段,status=1
时,展示文案1;status=2
时,展示文案2;这样设计的缺点是,如果以后要修改status=3
时,展示文案1,那么这个status
判断逻辑时写死在客户端,就没办法支持这种修改,且这种设计限定死了TextView
只能展示2种文案。推荐方案是后端直接将TextView
需要展示的文案下发,这样不管是status
的判断,还是文案的展示,后期都是可变的。
客户端不处理金额的计算
例如:外卖APP
,用户在下单的时候,需要选择收货地址,支付类型,优惠券等,任何一个选项的修改,都可能影响用户最后需要支付的金额。所以这里比较常见的接口设计是在每次选择完回到订单支付页面后,再发送一次请求,后端根据当前选项重新计算金额。金额永远是一款产品最重要,最敏感的信息,如果交由客户端计算,万一出错,即使少1分,都是毁灭性的,所以,关于金额,展示就好。
客户端少处理请求参数的校验与约束提示
例如:修改密码功能,密码规则"6-12字母,数字,下划线",有3种做法:
在发送请求前,客户端校验密码规则,如果不符合,则不发送请求。
优点:规则不满足时,可以减少不必要的请求。
缺点:客户端写死校验逻辑,密码规则变化时,客户端需要发版。客户端只判断
null
,和最短位数限制,其他校验规则交由后端处理。
优点:灵活性最好。
缺点:后端压力大,校验请求多。后端在通用配置的接口返回正则表达式,客户端获取后进行正则校验。
优点:具有一定灵活性。
缺点:开发,调试成本较高。(推荐:即使出问题,也可以清除配置,回退到第2个方案)
扩展性
接口的设计要具有一定的扩展性,考虑到后续版本变化,对于接口,字段的影响及变化。
接到需求之后,即使和策划或者产品经理确认了,他们说以后一定不会改变,但是千万别相信他们(程序员生存法则: 1.不要写死 2.不要相信策划)。我们的原则是:变与不变都能支持。
方案1:客户端在写页面的时候将左侧的"姓名","性别","年龄"写死,右侧的具体数据从json
解析获得
{
"name": "张三",
"sex": "男",
"age": "20岁",
"nickName": "小张"
}
方案2(推荐):将左侧的title
和右侧的value
,以list(key-value)
的数据形式进行下发,优点:左,右侧文案灵活配置,后期如果需要扩展,新增或删除一个条目,都可以通过后端控制。不过采用这种形式,也需要考虑实际场景,对于变化不那么频繁,数据item
较少,较固定的情况下其实没有必要设计的太灵活,只会增加开发成本。
{
"userInfos":[
{
"key":"姓名",
"value":"张三"
},{
"key":"性别",
"value":"男"
},{
"key":"年龄",
"value":"20岁"
},{
"key":"昵称",
"value":"小张"
}]
}
3.用flag
替换boolean
:一般情况下,一款APP
都会有config
接口,用于获取一些常量文案,通用配置等信息,会有很多类似开关的字段,如:isAdmin
,isPulsv
等等。
{
isAdmin:1,// 是否是管理员
isPulsv:1,// 是否是大v用户
}
优化方案:通过二进制第1位表示isAdmin
,二进制第2位表示isPulsv
。如果有其他新增状态,不需要新增字段,就需要改变返回的数据即可。
flag:3 // 二进制:11,表示2个状态都为true
安全性
响应数据中包含用户隐私的字段数据,需要加*
号。如:手机号,身份证,用户邮箱,支付账号,邮寄地址等。
请求参数中包含用户隐私的字段参数,如:登陆接口的密码字段,需要进行加密传输,避免被代理捕捉请求后获取明文密码。
客户端和服务器通过约定的算法,对传递的参数值进行签名匹配,防止参数在请求过程中被抓取篡改。
总是使用HTTPS
,没有例外。当今,只要连接了互联网,你的API
就能够在世界任何地方被访问。不是所有的访问都是安全的。有一些API
数据没有被加密,很容易遭到劫持并被篡改。
兼容性
在版本1.0.0
在使用接口/users
,如果在开发1.1.0
版本的时候修改了接口/users
的逻辑,在1.1。0
发版的时候线上就会出现两个版本的客户端访问同一个接口/users
,为了保证1.0.0
版本调用接口/users
不会出错,就需要通过version
字段或在路由中的/v1/users
,/v2/users
进行区分,不同版本客户端访问同一接口时处理逻辑要各自独立。
接口或者字段的删除、修改要谨慎:对于已经存在的接口进行修改,需要考虑对线上版本的影响,尽量是数据含义,和新增字段,而不是去修改。
性能优化
合并接口
为了减少客户端和服务器建立连接和断开连接消耗的时间、资源、电量,尽量避免频繁的间隔网络请求。
业务场景允许的情况下,尽量一个页面对应一个接口。原先一个页面要通过多个请求获取多种类型数据的情况,最好能通过一个接口全部获取得到。又如:在调用B接口前需要A接口的前置数据的情况,可以让后端支持下,在调用A接口时直接返回B接口的数据,减少类似这种的连续请求。
字段精简
定义字段名时,在保证良好可读性的前提下,尽量精简,减少流量的消耗
md5缓存
对于频繁调用且数据不常变化的接口,可以在返回的数据中添加md5
字段(用于校验除md5
外其他数据是否变化),在下次请求的时候将这个md5
作为参数传给后端,md5
没有变化的情况下,不返回data
,客户端可以直接使用上次请求缓存在本地的data
。
限制API返回的字段
API
调用者有时候不希望资源的所有字段都返回。如果能够在调用时,选择API
能够返回的字段,或者同个接口不同状态下需要返回的字段各不相同的时候,将减少网络传输流量,加速调用方的业务处理。
可以使用fields
查询参数来限制API
返回的字段。比如,以下请求只返回需要的字段信息: /users/12?fields=id,name,age
图片裁剪服务
客户端上传图片后,当需要在列表这些图片区域较小的地方展示的时候,没必要直接加载原图,可以先在后端通过图片裁剪服务处理后再进行展示。
局部刷新
一个页面,如果之前已经加载了20%的数据,那么就不需要每次都返回100%数据,只要返回剩余80%即可。例:社区帖子列表,每个列表中的帖子其实已经具有title、content、createTime
等字段,那么点击进入贴吧详情的时候,title、content、createTime
就可以从列表传递过来,详情页的请求只需要返回剩余数据,客户端需要额外处理数据组装逻辑,将前一个页面传递过来的字段和详情页请求到的字段组装成完整的model
数据。
wifi与移动网络的区别对待
WiFi连接下,网络传输的电量消耗要比移动网络少很多,应该尽量减少移动网络下的数据传输,多在WiFi环境下传输数据。例:crash
日志上报,数据统计接口等,可以在移动网络的情况下请求频率降低,或缓存,在wifi网络时上调请求频率,或将缓存的数据统一上报。还有上文提到的,如果是wifi网络状态下,就下发高清图提升用户体验,移动网络状态就下发缩略,或裁剪图。
体验优化
设计接口时,不能只考虑减少流量消耗,性能优化等,特定场景下用户体验的优化才是最高优先级的。
通过预加载降低对网络的依赖
在网络较差的情况。
例:列入要做电梯或者进地下室,这时候网络比较差的,查看帖子详情,可能就会加载不出来,提示网络错误等,影响正常体验。可以考虑在列表的接口中,将详情的数据一起请求下发,可通过md5
判断详情页面数据是否变化,避免重复加载,这样用户在网络比较好的情况下请求一次列表后,再进入详情页,就不再需要重新请求,对网络的依赖也是最小的。
前几页的帖子,用户查看详情的概率较高,可以在返回列表的时候携带正文内容,则可以实现秒开详情,也可以判断网络状态,wifi场景下可以将详情数据都返回。
错误代码
就像HTML页面出错了展示的错误消息一样,API在出错时也应该提供一个有用的错误消息给调用方。
API
应该总是返回合理的HTTP
状态码。API
的错误通常分为两类:客户端调用错误使用的400
系列状态码,和服务端处理错误使用的500
系列状态码。
一个JSON
错误消息应该包含这么几个部分,一个有用的错误消息,一个唯一的错误码(方便在接口文档里查到更详细的错误描述)和一个详尽的错误描述。
在开始设计的时候,
- 查询类的接口,应尽可能使用被动式提供数据的无状态接口,格式应尽可能使用对象。这样的接口对于扩展字段非常的方便,也很容易做到向下兼容。这样的增量接口,对于老的自动化测试来说,不会失效,仍然可以跑。
- 操作类的接口,尽可能地将资源分离,比如修改用户信息、修改用户头像信息、修改用户职位信息,这样的接口,尽可能使用独立的资源。
- 比较难搞的是数据结构设计不合理,那就是新增一个接口,只能尽可能避免。
对于实在没有办法需要全面升级接口的,如果可能,保持原有的业务,原有的接口运转正常,然后构建一套全新的隔离的接口,最后做下版本使用监控,当观察到所有用户都使用新版本的客户端的时候,并保持一段时间的时候,放弃对老版本的维护,继而下掉老版本的资源,当然,万不得已的时候还可以用强制更新。
接口统计功能
为了更好的了解我们用户的使用情况,需要我们尽可能多的收集客户端的信息,除了传统的IP
、User-Agent
之外,还应该收集一些移动相关的信息,比如:
- 手机操作系统,是android还是ios,都是什么版本
- 用户使用的网络状况,是2G、3G、4G还是WIFI
- 客户端APP是什么版本信息。