本系列:
- 从零开始的 Android 新项目(1):架构搭建篇
- 从零开始的 Android 新项目(2):Gradle 篇
- 从零开始的 Android 新项目(3):谁告诉你MVP和MVVM是互斥的
- 从零开始的 Android 新项目(4):Dagger2 篇
- 从零开始的 Android 新项目(5):Repository 层(上)
- 从零开始的 Android 新项目(6):Repository 层(下)
- 从零开始的 Android 新项目(7):Data Binding 入门篇
- 从零开始的 Android 新项目(8): Data Binding 高级篇
这回来讲讲后台接口的设计。
可能有同学会觉得后台的接口和我们大前端开发有什么关系?试想一下,在碰到一些不合理的接口设计的时候,你们开发是否觉得很别扭——需要为了坑爹的接口写很多脏代码引坑?甚至,这么开发出来的页面,体验也会很差?我们不是说硬无理要求后端接口按照前端业务去封装,而是说为了项目更好地发展,为了用户能有更棒的体验,应该有讨论商量的空间。一些差劲的设计,应该被拒绝。
本文使用前端来指代 Android、iOS 以及 Web。
本文不是教大家撕逼的(赶紧撇清关系)。
全局
全局指所有接口统一的规范。
请求头
应该使用http header来放置通用性的参数,比如:
- APPID(Android/iOS/H5)
- APPVER(版本号)
- APP-BUILD-NUM(内部小版本号)
- TOKEN
- NETWORK(网络环境)
- LANGUAGE(语言)
- 等等
前端使用 POST
键值对方式提交给后端,可以使用 RawJSON
格式。
Content-Type
设为 application/x-www-form-urlencoded
或者 application/json
。
全局响应格式
响应格式应该统一,方便前端做统一的处理,尤其是数据字段,应该统一放在一个map里面。
名字 | 类型 | 详细描述 |
---|---|---|
status_no | INT | 状态码 |
status_msg | STRING | 状态信息 |
data | MAP | 响应内容 |
time | INT | 响应时间戳 |
状态码
全局应该定义统一的状态码(status_code),而不应该每个接口单独去定义。
具体规则可以自行定义,比如0为正确,负数为错误。
常见的错误状态码有
- 普通异常
- token不合法,需要重新登录
- 重复登录
- 需要完善个人信息
- 第三方账号登陆,需要绑定官方账号
- 请求头不合法(版本号,APPID等)
- 数据解密错误
可以根据错误类型划分使用的区域段,如登陆系列使用 -1000 到 -1999 区域。
如此定义后,前端可以进行全局的统一处理,如重复登陆则踢出用户。
错误信息
除了特殊的错误信息——如重复登录、token不合法这些状态码对应的,以及无网、没数据这些,对于通用的异常,应该由后台返回错误信息。
统一data字段
data 字段应该统一放在一个 map 内,里面存放具体的响应信息。
Scheme
全局定义统一的 scheme(Deeplink),方便前端进行跳转。
前端只需要定义自己唯一的 Deeplink 并进行注册即可(scheme 和 host)。
具体使用 REST 风格(如 markzhai://article/XXX),还是普通的 urlencode (如 markzhai://article/?id=XXX&redirect_url=XXX)可以根据自身需求定义。
使用 REST 风格的一个顾虑是可能 scheme 本身并不是基于资源的,而是基于类型、行为等,所以 urlencode 可能更通用,但相应地基于 Deeplink 的资源索引会希望你是无状态的 REST 风格。
回传 or 状态码
应该使用回传还是状态码呢?比如点赞消息,是应该回传一个 status_code,0则表示点赞成功,还是应该回传现在的赞状态呢?
其实这两者对于后台的性能来说,是几乎没有影响的,因为取得的只是修改的字段的最后结果。但是对前端来说,差别就有了——需要维护状态。
举一个例子:
A 和 B 是两个用户,B 关注了 A,A 没有关注 B。
A 看 B 的主页的时候,显示关系是 未关注,此时 A 点击了关注,如果没有回传信息,那么我们只能把关系刷新为 已关注,而没有足够的信息去刷新为 互相关注。否则就需要前端去做恶心的逻辑(后端一开始用户关系就需要传 B 关注了 A),根据原来的关系去做切换,还要在失败的时候刷回原来的状态。
一些有丰富经验的后端会在这种接口使用回传,因为他们知道区别。
模块vs页面
在后台的接口设计上,又分为了按页面以及按模块。
按页面的接口尽可能让前端一个页面只请求一次,一次返回所需要的全部信息;按模块的接口在后端定义自己的业务模块如用户、Feed、标签、搜索等,并尽量避免模块间的耦合。
从后端角度来说,按模块当然是更好的(只需要划分地够细就好),到时候需求有什么变更,让前端自己去改变接口的组合就好,自己高枕无忧。但从前端的角度来说,接口的组合涉及到异步之间的关系,尽管RxJava这样的响应式编程框架让异步简单了很多,但仍然希望可以避免,更严重的是,多次接口请求会让前端的体验变差,并行接口的影响稍小,而一些有前置后置关系的接口则麻烦比较大,一个接着一个请求,会让用户等很久。即便是并行接口,有时候页面的渲染仍然需要所有接口数据返回后才可以进行。
但如果让后端按照页面去套,这样在后端其实一样有性能的损耗,需要一个页面接口去单独调用各个模块的接口,然后进行组合。
究竟如何选择呢?笔者认为在服务器性能足够的前提下,后端应该尽量减少页面请求次数,尤其是有依赖关系的串行请求。另一方面,在一些影响不那么大的页面,则可以由前端自行进行接口组合(比如上面是用户主页的用户展示,下面是该用户的 feed 列表)。
另外,如果你们有一个好的设计师,那么他应该会贯彻一个地方只应该以一样东西为主体,而不应该去把乱七八糟的东西拼凑在一起。
分页信息
现代的前端交互上,已经很少会有页码显示了,所以很多后端的列表页接口中,就没有带上了分页的信息,而改让客户端去维护请求的页码。
那么,分页信息在接口中,真的就没有存在的必要了吗?其实未必。
为什么需要分页信息
页面大小(pageSize)可能改变(无论是前端自己的配置亦或是后台修改),如果仅由客户端维护页码,那么下次请求下一页就会出错,除非客户端带上自己上次的页面大小。
如果客户端不知道当前页码和总页数,就无法在请求完判断底部应该显示上拉加载更多还是没数据了,导致必须再请求一次,根据是否返回 list 以及数据是否为空去进行判断。
另外,由后端返回页码也避免了客户端修改页码出错的可能。
但对后端来说,这些信息的获取却意味着更大的计算和I/O资源损耗。
折中办法
折中地,可以让后端返回一个 has_more
字段,这样可以避免最后一次不必要的请求(尤其是数据都不够显示满一页的情况下),体验会好很多。尽管这样仍然无法避免页面大小改变的问题。
配置
一些后台喜欢让让前端写限制逻辑,比如搜索的关键字限制,各种过滤逻辑。
咱们先不提让前端写死这些逻辑的灵活性问题(客户端和网页不同,不能那么方便地发版本,即便是网页,改代码发版本就不用测试了吗?出了问题你背?)。前端的输入真的可以信任吗?且不谈代码可能写的不够严谨导致输入跳过了检查,用户还能root、越狱,甚至可以反编译客户端或者直接模拟请求。
所以良好的配置检查应该有两种
- 后端下发配置字段,前端根据字段去做对应检查。好处是减少后台压力,坏处是无法保证安全性。
- 后端收到请求自行检查过滤,如果出错则返回错误信息给前端显示。
毋庸置疑,后者更好。
另外,再说说灵活性。今天可能限制3个字,明天产品需求可能就是4个字,现在产品/运营说不会改,到时候难道就真的一定不会改吗?
空字段
一些空字段,如果没有,服务端应该返回一个空的默认字段 比如 String 用””,int 用 0,Object 用 {},Array 用 [],这样减小前端校验某些校验漏了出现错误的情况(由三帅泥阿布补充)
我个人认为这样对流量损耗不大,且确实避免了很多可能的异常,是个很好的意见。当然了,正如后端不应该相信前端的输入一样,前端也不能相信后端数据的完备性,仍然还是需要悲剧地去校验。
教训
- 不要相信什么以后重构,接口现在这么说,以后他会告诉你,没法兼容老版本所以只能这样了(甚至搞出两套规则让你同时兼容)。
- 不是说后端就是老大。大家的目标都是为了项目能做好,而现在通常前端的压力比后端更大(前端写得头昏脑花,后端网上东逛西逛),所以在不会很大影响性能的前提下,应该满足前端的合理需求。体验为先。(硬气一点,老大应该挺你,甚至亲自去撕逼,大不了找CTO)
- 接口的频繁修改要向上反馈,测试数据不满足要求也要及时提出。咱们不做背锅侠。
- 灵活,灵活。做各种需求的时候,想一想,这儿会不会改变?就算现在不会变,以后就不会变吗?比如抽屉里的入口,是不是要做成可配置的?多问问,实现上尽量灵活。
总结
本篇讲了很多通用的后端接口设计问题。帮助大家在面对一些不合理的接口设计时,能进行友善的讨论(撕逼),让项目能做得更好。欢迎各位在评论里或者通过邮件(zhaiyifan56@gmail.com)补充其他点,我会标注出来源。