从零开始的 Android 新项目(9):前端用后台接口设计

598 查看

本系列:

这回来讲讲后台接口的设计。

可能有同学会觉得后台的接口和我们大前端开发有什么关系?试想一下,在碰到一些不合理的接口设计的时候,你们开发是否觉得很别扭——需要为了坑爹的接口写很多脏代码引坑?甚至,这么开发出来的页面,体验也会很差?我们不是说硬无理要求后端接口按照前端业务去封装,而是说为了项目更好地发展,为了用户能有更棒的体验,应该有讨论商量的空间。一些差劲的设计,应该被拒绝。

本文使用前端来指代 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)补充其他点,我会标注出来源。