15_移动端项目或者前后端分离项目接口规范
写在前面的话,主要是谈谈接口
随着前后端的分离,后端工程师不需要编写页面,甚至不需要编写JavaScript代码,只需要提供接口给前端工程师即可,可是就是仅仅一个接口,对于很多后端工程师而言,在实际开发中,不能一次成形,导致前端工程师们对接接口的过程中,依然问题重重,很多后端同事认为,我在接口里面只需要把数据返回给前台,其他我一概不管,这样造成的后果就是
1、接口结构无序、杂乱无章
2、接口和实际业务场景、UI或者原型不匹配、不可用、等于白写、浪费时间
3、前端同事在接口对接过程中频繁的与后端同事沟通接口问题,严重的甚至需要多次沟通才能得到一个完整无误的接口,最终导致简单的问题复杂化了,前后端都很恼火
4、事情没做好
后端在编写接口前,首先是对项目整体业务的理解,在对业务未理解透彻之前,编码都是无意义的,其次就是,后端的同事在编写一个接口之前,应该先结合下已有的UI或者原型分析,如果说发现UI或者原型与现有业务不匹配的情况下,那么应该主动找到设计师以及前端的同事沟通处理,沟通才是第一位,有的时候沟通往往比技术更重要,这在团队合作开发项目的过程中尤为重要.
后端在开发接口时,我觉得主要需要注意以下几个方面:
接口请求方式
接口传参
全局错误码定义
接口返回json格式约定
接口文档编写
下面我将围绕这几个方面逐一展开说明,注:以下内容中红色字体标明的内容是后端同事必须遵守的规范,也是开发过程中不应该出现的低级错误.
接口请求方式
关于接口请求方式,目前比较常用的有:GET、POST、PUT、DELETE,对应数据库的增、删、改、查操作
GET(SELECT):从服务器查询资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器端更新资源(客户端提供改变后的完整资源)
DELETE(DELETE):从服务器删除资源
由于公安部在做网络安全漏洞扫描时,会把PUT、DELETE请求(除GET和POST之外的接口请求)识别成漏洞,所以,后端同事在开发接口时,请尽量保证只使用GET、POST这两种请求方式.
接口入参规范
1、get请求只支持query形式的参数,不能传递数据量很大的参数
${serverUrl}/app/api/login?loginId=xxx&passwd=xxx
2、post请求分为postForm和postJson两种传参方式
postForm:postForm在B端表单提交比较常用
postJson:在客户端可以直接传递一个序列化的实体到服务器,非常方便,并且不容易出现中文乱码的错误
- postForm
@FormUrlEncoded
@POST
Observable<RawResponse> postForm(@Url String url, @HeaderMap Map<String, Object> header, @QueryMap Map<String, Object> queryParams, @FieldMap Map<String, Object> fieldMap);
2022-11-04 09:52:35.758 20660-20698/com.aykj.mall D/OkHttp: --> POST https://www.wanandroid.com/app/api/login http/1.1
2022-11-04 09:52:35.758 20660-20698/com.aykj.mall D/OkHttp: Content-Type: application/x-www-form-urlencoded
2022-11-04 09:52:35.768 20660-20698/com.aykj.mall D/OkHttp: Content-Length: 22
2022-11-04 09:52:35.768 20660-20698/com.aykj.mall D/OkHttp: isDowload: false
2022-11-04 09:52:35.768 20660-20698/com.aykj.mall D/OkHttp: isUpload: false
2022-11-04 09:52:35.768 20660-20698/com.aykj.mall D/OkHttp: loginId=xxx&passwd=xxx
- postJson
postJson会将传递的参数包装成一个JSonObject,传递给后端,所以后端可以直接用一个java实体来接收
@POST
Observable<RawResponse> post(@Url String url, @HeaderMap Map<String, Object> header, @QueryMap Map<String, Object> queryParams, @Body Object body);
2022-09-01 10:09:53.551 21155-21199/com.aykj.mall D/OkHttp: --> POST https://www.wanandroid.com/app/api/login http/1.1
2022-09-01 10:09:53.551 21155-21199/com.aykj.mall D/OkHttp: Content-Type: application/json; charset=UTF-8
2022-09-01 10:09:53.551 21155-21199/com.aykj.mall D/OkHttp: Content-Length: 32
2022-09-01 10:09:53.551 21155-21199/com.aykj.mall D/OkHttp: isDowload: false
2022-09-01 10:09:53.552 21155-21199/com.aykj.mall D/OkHttp: isUpload: false
2022-09-01 10:09:53.552 21155-21199/com.aykj.mall D/OkHttp: {“loginId”:“xxx”,“passwd”:“xxx”}
3、请勿在query参数上要求前端传递实体到后端,错误的做法,传不过去
上面这张图是一个反例,数据量很大的情况传不过去,这种情况请使用postJson方式,后端直接从前端接收一个实体很方便
4、接口入参一定要注释清楚,更不能没有,如查询栏目,一定要注明栏目id传值的取值情况
上面这张图是一个反例,这样的接口直接给到前端的话,前端很头疼😂。
全局错误码定义
{
"code":200,
"message":"ok",
"result":"返回结果"
}
- code:响应的状态码,注:这里的状态码不是服务器的http协议状态码,而是业务相关的状态码,比如接口请求成功为200,一般情况下code以2开头表示成功,不以2开头表示失败,取决于自己部门内部定的框架的规范,例如301表示用户名不能为空、302表示验证码输入错误等等…
- message:响应的提示信息,前端会将改字段的内容作为提示信息显示给用户。
- result: 返回给前端的业务数据,可以是实体,可以是数据,也可以是基本数据类型,取决于实际业务,后面会详细展开说明。
{
"code":301,
"message":"用户名不能为空",
"result":null
}
{
"code":302,
"message":"验证码输入错误",
"result":null
}
…
后端在编写接口时,返回的数据结构最外层一定要统一,同一个项目中,开发人员A返回的JSON结构为:{
“code”:200,
“message”:“ok”,
“result”:…
},开发人员B返回的JSON结构为:{
“statusCode”:200,
“msg”:“ok”,
“result”:…
},这样的情况禁止出现,否则后果自负
业务中同样的错误信息,code和message应固定或者说保证一致
业务中出现的状态码(code)与业务提示信息(message)应保持同一,并且一一对应
用户授权
前后端在开放用户授权时,以用户登录、注册为例:
后端在接受到前端提交的数据后,应该从数据库反查判断这个用户是否已经注册
1、如果反查不到这个用户,说明用户未注册,返回相应的状态码和业务提示信息,告诉前端,当前的用户未注册,前台会跳转至用户注册页面引导用户注册
2、如果反查到了这个用户,说明用户已注册,那么此时应该走登录的流程,后端需要使用反查到的用户信息通过JwtToken生成一个token以约定好的json数据格式返回给前端,前端会以此token作为用户登录态的数据持久化
3、最后后端编写其他业务接口时,所有涉及到登录拦截的地方,前端会将缓存起来的token,传递给后端,后端通过校验改token是否有效,以及解析该token,反查数据库判断用户是否已注册,来实现登录拦截
4、后端在编写接口时,如果希望前端标识当前用户信息,那么前端同样会将缓存起来的token传递给后端,后端通过解析这个token来确定当前请求接口的用户是谁,而不应该让前端传递明文的userId等信息给后端,如果解析token失败,那么就说明用户的登录态失效了,此时就要重新走登录或者注册的流程,也就是登录拦截
接口json格式规范
1、json格式需固定
例如如下图形
如上图所示,横向是时间,纵向是温度的value值
我们给出的json结构应该如此:
{
"code": 200,
"message": "请求成功",
"result": [
{
"time":"2022-05-17 05:39",
"value":10
},
{
"date":"2022-05-17 06:42",
"value":8
}
//more...
]
}
在工作中,我们经常碰见这样的数据格式:
{
"code": 200,
"message": "请求成功",
"result": [
"2022-05-17 05:39":{
value:10
},
"2022-05-17 06:42":{
value:8
}
//more...
]
}
这里所说的json格式固定主要针对此种情况,后端给到前端的接口格式必须是固定的,所有动态数据值都需相应的key与之对应
2、需要顺序显示的内容返回值为中文key:value 的对象。这个结构没有索引没有顺序,且中文key这种低级的不符合前后端规范的错误希望能够规避。这种数据前端处理很复杂影响性能,代码不必要的逻辑增加,代码冗余。
{
"code": 200,
"message": "请求成功",
"result": {
"结题": "2022-12-15",
"初期": "2022-10-01",
"中期": "2022-11-01",
"开题": "2022-11-15",
}
}
像这种有多个内容,且有顺序的数组应返回结构如下
{
"code": 200,
"message": "请求成功",
"result": [
{
"name": "初期",
"value": "2022-10-01"
},
{
"name": "中期",
"value": "2022-11-01"
},
{
"name": "开题",
"value": "2022-11-15"
},
{
"name": "结题",
"value": "2022-12-15"
}
]
}
贴上前端处理的代码,这些不必要的逻辑代码都可以省略的。
let data = jsonData.result
let stepArr = []
for(let key in data) {
stepArr.push({
"name": key,
"value": stepArr[key]
})
}
//...
stepArr.sort()
3、返回json数据本身是一个数据或者对象,转成该结构再返回,避免直接返回字符串等前端处理
{
"code": 200,
"message": "请求成功",
"result": {
"seoTitle": "云玺大宅别墅轻奢装修效果图",
"seoDescription": "云玺大宅别墅轻奢装修效果图",
"searchOptionRoot": "69,70,71",
"brandImagesPath": "[{\"ext\":\"jpg\",\"fileName\":\"dDf7E2LRQ6eAavlYAAGaTQsy5y0442.jpg\",\"fileSize2\":\"108KB\",\"sysCompanyCode\":\"A01\",\"path\":\"group1/M00/1D/99/dDf7E2NgtayAWajhAAGvY9nTaaQ531.jpg\",\"createBy\":\"zhaomin\",\"fileSize\":108,\"enable\":1,\"sysOrgCode\":\"A01A14A06\",\"id\":\"ff808081841de9db018431cf8af40437\",\"contentType\":\"image/jpeg\",\"createName\":\"赵敏\",\"createDate\":1667282995000}]",
"brandLogoPath": "[{\"ext\":\"jpg\",\"fileName\":\"dDf7E2LQv3KANrzNAAIaT7BPDf0412.jpg\",\"fileSize2\":\"156KB\",\"sysCompanyCode\":\"A01\",\"path\":\"group1/M00/1D/99/dDf7E2NgtWaABhy8AAJsfhZXSYA050.jpg\",\"createBy\":\"zhaomin\",\"fileSize\":156,\"enable\":1,\"sysOrgCode\":\"A01A14A06\",\"id\":\"ff808081841de9db018431ce78cf0436\",\"contentType\":\"image/jpeg\",\"createName\":\"赵敏\",\"createDate\":1667282925000}]",
}
}
上面的json结构中,主要看brandImagesPath和brandLogoPath这两个字段,对象数组直接返回了字符串
let brandImages = res && res.brandImagesPath ? JSON.parse(res.brandImagesPath):[]
let brandLogos = res && res.brandLogoPath ? JSON.parse(res.brandLogoPath):[]
前端现在需自己转成需要的格式,多此一举,这点简单东西后端应该处理好再返回。
4、后端在改接口或者业务有变动的时候 可以在原有实体的基础上扩展属性 但是不要改实体里面原有的属性 也不要改整个json数据的结构 不然前端已经绑定好的页面也出错了
举个例子说明
{
"code": 200,
"message": "请求成功",
"result": {
"permitId": 123456
}
}
前端已经有一个1.0版本上线并正常使用了,项目迭代到2.0版本,permitId需要返回多个值给前端处理,后端二话不说直接把接口中permitId的结构改成了数组:
{
"code": 200,
"message": "请求成功",
"result": {
"permitId": [123456, 789000]
}
}
这样粗暴的修改,就会导致线上1.0版本出现错误,正确的做法应该是在原有基础上扩展一个字段,这样就可以保证在扩展2.0版本业务的同时,1.0线上版本能够正常运行:
{
"code": 200,
"message": "请求成功",
"result": {
"permitId": 123456,
"permitIds":[123456, 789000]
}
}
接口文档编写
接口文档是前后端对接重要依据,后端写明接口文档,前端根据接口文档对接。
接口文档需注明字段对应页面内容。不写接口文档的坏处:
第一:前后端开发没有标准,没有依据。
第二:容易扯皮,没法追踪,职责不清。
第三:开发效率低。等等。
文档形势目前主要分几种:
1、依赖swagger框架,自动生成接口文档(swagger只能生成基于key-value详细参数方式,针对json格式,无法说明具体请求内容)
2、使用Knife4j,也就是swagger的升级版
3、手动编写说明文档,推荐markdown编写
接口对接
万事俱备,只欠东风,虽然上面我们准备了所有我们该准备的,接口定义完美无缺,接口文档也已说明,但在对接时任然可能出现问题,此时我想我们还需注意细节,那就是后端接口需自行进行测试,推荐使用PostMan进行测试,作为接口调试神器,Postman大名想必大家都已知道,就不多说了。