tomorrow.cyz@gmail.com
app和后端的交互,一般都通过后端提供的api实现。一个良好的api设计, 可以明确边界,清晰架构。以下是我对移动端API接口设计的一些认识。
1.命名
好的API命名(包括参数名字)可以省却好多沟通交流的工作,绝对值得为每个API起一个好名字。
命名尽量统一规范。不要有些地方用getCatogaries,而其它的地方用Shops 这样的情况(使用RESTFul API规范可以部分避免这种情况)。API就好比合同,里面每个字母/词 都值得认真推敲。
理想的情况,API开发之后,开发者和使用者都容易默写出API的名字。
2.文档化
所有的API,必须文档化,而且必须是可以做版本管理的文档化。可以使用wiki。
API的文档里面应该功能说明,参数说明,调用URL,调用实例,接口返回数据。
尽量保证可以方便地测试。
再细小的API改动都需要先改动文档。
3.内容协商
如果一个接口根据不同的请求参数返回不同内容,此时需要内容协商。
典型的比如根据屏幕大小或者分辨率甚至网络环境返回不同的图片,通常通过扩展客户端User-Agent, 在User-Agent里面将屏幕信息包含进来,后端根据User-Agent来返回不同的图片。
再比如一个接口提供protocol和json两种数据格式,比较合适的方法,通过Accept头部,而不是自定义 头部,或者query参数来实现内容协商。这样做对vanish缓存比较友好。
4.服务端实现和客户端实现的权衡
需求里面,经常会碰到有一段逻辑,可以在服务端实现,也可以在客户端实现。这里因为立场不同,又 受限于各种历史包袱,再加上所谓的胖瘦之争,非常容易有争议。个人倾向于,工作量影响不超过1天的 情况下,尽量在服务端实现。
原因在于服务端天然具有便于部署的属性,另外也比较便于不同的平台保持一致性。
5.缓存的考虑
在设计API的时候充分考虑缓存的问题,可以在同等硬件环境下做出更好的负载来。
缓存包括客户端本地缓存和服务器端缓存。
客户端本地缓存,举个例子,我们有个请求补丁的接口checkPatch,最简单的设计就是传我的App版本上去, 服务器端返回Patch对应的信息。改进一点的话,就会把我本地已经下载过的patch的版本信息或者md5带上, 服务器端如果没有更新,就不需要返回Patch对应的节点了。这其实和http 302/if-match的思路是一致的。
类似的比如,有的接口实现的时候,请求的时候带上上次返回数据的hash,服务端将接口计算结果与上次比较, 如果相同,就不需要返回具体数据,返回hash就可以了。
大的资源请求,比如请求banner图片的数据,一定要有类似带上md5这样的机制,不用每次下载更新。当然,也 要考虑怎么一个机制让本地数据失效。
另外,通用的http缓存机制(cache-control),仍然可以用。
另外一个考虑是服务器端缓存,3中Accept头部那个例子其实也是考虑服务器端缓存的一个例子。
另外一个例子,定义API的时候,最好把参数顺序固定下来,客户端严格按照这个顺序来拼接参数,这样有利于 服务端实现vanish的缓存策略。
这里后端的同学应该比较擅长,特别是调用频繁的接口,要有这根弦,
6. by function 还是by page的问题
假设我有一个页面既要显示用户的基本资料(如nickname,头像,等级等等),又要显示该用户的近期历史活动 记录,而这两者从业务上来说属于不同的功能模块。那么我是应该提供两个API (by function),一个用来获 取用户基本资料,另一个用来获取用户的近期历史活动记录呢?还是提供一个大而全的API (by page),用来 提供该页面所需的所有信息呢?
从服务端的架构来说,毫无疑问,by function是低耦合的,架构非常清晰。
但是对于移动客户端来说,by function需要多次网络交互,效率不高,也会给客户端实现增加复杂度。通常 这种情况尽可能通过在服务器端gateway的方式来解决。最终提供by page的接口。
7. 数据格式
高并发,非常关注性能的话可以采用ProtoBuf。创业公司为了方便调试,可以使用json。
使用json在处理空值的时候要小心。另外,json没有Date这样的数据类型,建议使用时间戳来表示,不要使用String, 转化的时候容易出错。
对于数组,到底是从0开始技术,还是从1开始计数,要统一。
建议统一最外层的结构(也就是通常所说的envelope),比如
{
data{
key:value
}
info ""
status 1
}
8.接口安全性
8.1 对有敏感数据的接口使用HTTPS,当然,如果全站使用HTTPS更好
8.2 接口签名
一般通过给客户端分配一个appid和app-secret,然后在调用接口的时候利用这两个参数,以及请求的参数,利用一定的规则( 最简单的比如请求参数首尾加上时间戳及app-secret进行md5,尽量采用私有的规则)进行签名,在服务端对接口进行验签。
8.3 分清楚哪些接口是需要用户登录才能访问的
8.4 token
这一点和服务器端采用的鉴权设计架构关系较大,通常的做法是,用户登录成功后,服务器端会返回一个token(或者session_code 之类的),以后客户端访问需要用户登录才能访问的接口,就带上这个token。
这个token会有一个Expire机制,Expire了,会重新要求用户进行登录。大部分实现还有renew操作,可以为token续期。有的设计 还专门区分了access_token和refresh_token。
为了进一步增加安全性,还可以在传输token的时候,通过算法以一些参数进行加密,让每次传输的token不一致,在服务端解密。有 的还设置了公钥私钥机制。
客户端还需要考虑token持久化的安全问题,保存的时候进行加密,取出来进行解密。
用户登出的时候,token失效。
所有的这些机制,都只是增加攻击成本,并没有绝对的安全。
8. 其它
8.1 尽量将API部署在专用域名之下
8.2 将API的版本号放入URL
https://api.example.com/v1/zoos
8.3 如果记录数量很多,提供参数过滤返回结果
如
?litmit=10
?page=2&per_page=100
8.4 接口定下来以后,改动尽量谨慎。尽量考虑是不是可以通过现有接口来实现。
比如我们在做热修复的时候,之前有一个传统方法的修复方式,实现的时候,因为对新的解决方案还不是特别有把握,想保留 现有的机制,所以在check补丁包的接口里面考虑返回一个新的节点,用于描述新的补丁包。后来经过讨论,不改接口就可以 实现,有两种机制,一种是根据后缀名或者读文件格式来确定是新补丁还是热补丁,另一种更智能,直接在返回的补丁包里面 包含新补丁和老补丁,然后在包里面有个规则文件(最简单的比如blacklist),通过客户端来适应这种变化。
这个例子的一个思路是,如果接口已经上线,那么尽可能只改动一端(客户端或者服务端),来实现需求。