首页 > 代码库 > 用产品思维设计API(一)——RESTful就是个骗局
用产品思维设计API(一)——RESTful就是个骗局
用产品思维设计API(一)——RESTful就是个骗局
前言
最近公司内部在重构项目代码,包括API方向的重构,期间遇到了很多的问题,不由得让我重新思考了下。
- 一个优雅的API该如何设计?
- 前后端分离之后,API真的解耦分离了吗?
- 不断的版本迭代,API的兼容性该如何做?
年前,我司内部的接口已经进入了一个完全的重构阶段,参考了市面上各大平台的API和文档,自己也总结出了很多的心得。这里向大家分享一下,接下来一个月,我们向从下面几个方面向大家介绍一个优雅的API(至少我认为挺优雅)该如何设计。
- RESTful就是个骗局
- 数据解耦,才是前后分离的本质
- 版本控制,没有你想的这么简单
- 随意定义错误码,你还在这样干?
- 安全,就只能用HTTPS?
ps. 打一个广告,公司内部现在在招聘各种技术岗位,Java、Android、前端等,待遇保证能让你涨30%,有兴趣的朋友可以加我微信,二维码在文章最后。
Ok,今天是第一篇文章——再看RESTful。
回顾一下HTTP协议
基于HTTP协议的API使我们在开发APP、网站中最常见的形式,为了更好的了解如何设计一个良好的API,我们这里先简单的回顾一个HTTP协议。
先抓包看一个请求demo
我们用Fiddler抓了一下360浏览器的任务中心的API接口信息,如下是它的请求信息:
POST http://task.browser.360.cn/online/setpoint HTTP/1.1
Accept: */*
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Pragma: no-cache
Content-Type: application/x-www-form-urlencoded
Host: task.browser.360.cn
Content-Length: 449
Cookie: T=s%3Ddaf1a2e6347e01ebccc72d639441f9ef%26t%3D1456561881%26lm%3D%26lf%3D1%26sk%3D03714c868adc12684c89a65c05bc7709%26mt%3D1466821360%26rc%3D4%26v%3D2.0%26a%3D1; zsmodel=MI%20NOTE%20LTE; zsosv=4.4.2; __guid=243694361.1257306006931263000.1482113838601.9001
stamp=1482151473&qt=Q%3Du%3Dfgpubh%26n%3D%25PO%25QR%25P9%25R1%25P0%25RS%25O5%25P4%25PO%25N7%25O9%25S8%26le%3Dp3EwnT91WGDjZGLmYzAioD%3D%3D%26m%3DZGZlWGWOWGWOWGWOWGWOWGWOZwDl%26qid%3D29340825%26im%3D1%5Ft013ba372ccf308e7b6%26src%3D360se%26t%3D1%0D%0AT%3Ds%3D76f8c61854f171f63f66a8f552962e8e%26t%3D1456561881%26lm%3D%26lf%3D1%26sk%3De79c0ecb263d447d96e85f23825c3924%26mt%3D1466821360%26rc%3D9%26v%3D2%2E0%26a%3D1&verify=6fce1222e64cefa2b5b3d24d65fa9eb1
得到的服务器响应结果,如下所示:
HTTP/1.1 200 OK
Server: nginx/1.6.3
Date: Mon, 19 Dec 2016 12:42:58 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: close
{"errno":"0","errmsg":"success","lastpoint":"2016-12-19-20-42"}
这里,没有任何文档我们也能够直接的看出来。大概步骤是:
1. 浏览器向 http://task.browser.360.cn/online/setpoint 发起了一个 POST请求
2. 该请求中附带了一些cookie信息,也带了一些自定义的消息体
3. 响应结果是正确的,返回给浏览器一个JSON格式的数据。
Http协议的构成
对于一个完整的请求(Request)、响应(Response)来说,还是有一定的套路的,这里我们看一下HTTP请求和响应的规范格式。
对于上面360浏览器的demo,结合两个协议图文,我们能够看到更多的信息:
- 请求Request
- 请求的方式 POST
- 请求的地址 URL
- 版本号
- 请求的头部信息,headers(cookie\UA等在于此处存着)
- 附属体信息 (通常为自定义上传的信息)
- 响应Response
- 版本号
- 状态码 (http协议的状态码)
- 相应头部信息headers (时间、数据格式、编码等信息)
- 附属体信息(通常为相应的自定义数据体)
OK,既然我们已经了解了HTTP协议的请求与响应的构成,理论上我们已经可以利用上述的逻辑加入完成自己的API设计了。
设计之前,我们需要思考一下,我们需要设计吗?目前市面上最流行的RESTful API协议为什么我们不用?
再看RESTful
在API设计上,如果有一样东西获得广泛认可的话,那就是 RESTful 原则。
REST的关键原则与将你的API分割成逻辑资源紧密相关,并采用所用并理解的原理。使用HTTP请求控制这些资源,其中,这些方法(GET, POST, PUT, PATCH, DELETE)具有特殊含义。
举例来说,有一个API提供公司内部(company)的信息,还包括各种部门(departments
)和雇员(employees)的信息,则它的路径应该设计成下面这样。
https://api.example.com/company
https://api.example.com/departments
https://api.example.com/employees
基于请求的方式和路径来作为常见的CURD(增删改查)
GET /company:列出所有公司
POST /company:新建一个公司
GET /company/ID:获取某个指定公司的信息
PUT /company/ID:更新某个指定公司的信息(提供该公司的全部信息)
PATCH /company/ID:更新某个指定公司的信息(提供该公司的部分信息)
DELETE /company/ID:删除某个公司
GET /company/ID/employees:列出某个指定公司的所有雇员
DELETE /company/ID/employees/ID:删除某个指定公司的指定雇员
对于请求后的响应结果,RESTful也做了一个很好得定义:
GET /collection:返回资源对象的列表(数组)
GET /collection/resource:返回单个资源对象
POST /collection:返回新生成的资源对象
PUT /collection/resource:返回完整的资源对象
PATCH /collection/resource:返回完整的资源对象
DELETE /collection/resource:返回一个空文档
RESTful API设计确实比较不多,对于一些简单的APP来说,能够快速的开发,并满足他们的绝大部分需求。
但是,请来一个神,就必须将神供着!
我们在使用之中也出现了很多头痛的问题,如:
问题1:多表多条件联查接口,如何设计?
很多情况下,我们的API并不是简简单单的查询(select)数据库中的数据,直接返回给调用者。我们可能会涉及到多表联查(left join , inner join)、排序(order by)、条件判断(where)、合并(union)等等。
如:找一个在我公司中找32~40岁,月收入在8000元,且在IT部门的人,属于北京事业群,将他们的工作KPI按逆序排列输出。
N多条件在一起的时候,API请求路径就会变得很难设计。
当然,网上有采用如下方式
/api/company?field1=abc&field2__like=%abc%&field3__gt=100&field4__not=999
但是,调用者调用API的时候,就感觉像是写SQL,API本身亦变得不可维护。
问题2:多逻辑请求接口,很难命名
不要小看命名的问题,我们要从名字上便于理解,又不要太长,又不能随意的缩写,其实很难。
如针对上述我们要求查询的逻辑来说,整个结构请求的路径就会变为如下所示:
GET /company/:ID/departments/:ID/employees/:ID/AREA/:ID/KPI/:ID?agemin=32&agemax=40&income=8000&order=api&sort=desc
虽然能够请求,但是已经很难从请求路径上看出我们的请求目的是什么,背离了所见即所需求的目的。
问题3:完整资源对象的返回,并不是调用者所需要的
如果一个数据库的字段很多,如我们的产品表,将近40~50个字段,每一个字段的类型、输出格式化都不一样,这个时候,如果直接将bean打印出来,如下所示:
出现几个问题
- 太多冗余字段:APP需要的PC不需要,PC需要的,H5不需要
- 每个字段都很难理解:调用API的人,估计看文档要看疯了,需求稍微一遍,前端样式也要跟着改,再看一次文档(韬哥认为这个是最重要的)
- 整个库设计完全暴露:安全安全!
{
"totalCount": 15,
"data": [
{
"id": 7956,
"sTitle": "信证-通汇安盈一号",
"productTypeId": 3,
"zdPrice2": 2,
"nianHuaShouYiStart": 8.5,
"nianHuaShouYiEnd": 9.5,
"jdt": 100,
"touZiLingYu": "基础设施",
"level": 99,
"saleStatus": 121,
"category": 30,
"jdTime": "2016-12-20 09:48:13",
"raiseProgress": "【2016年12月20日9时更新】本期为第六期,本期规模不限,本期已封帐,目前年前总剩余额度2520万,需要资产证明,有下期,下期无缝对接中;",
"touZiMenkan": 100,
"collectCount": 1,
"hasCollect": false,
"redPack": "",
"fundType": "基础设施",
"visitCount": 24631,
"docPreviewCount": 11,
"producttagids_intarray": "\t13\t11",
"productTags": "11,二年期;13,固定收益类;",
"title": "信证-通汇安盈一号(第6期)",
"bid": 3,
"statusId": 40,
"qiXian": 24,
"peibi": "",
"pmName": "王佳慧",
"pmUserName": "王佳慧",
"daXiao": "小额畅打",
"lingYu": null,
"shouYiType": "固定收益类",
"payStatus": "半年付息",
"downLoad": "Upload/ProductPDF/20161202/信证-通汇安盈_473.zip",
"groupName": null,
"zdPrice": 1,
"addr": null,
"companyId": 272,
"adminId": 473,
"groupMaxPrice": 0,
"nianHuaShouYiExt": "",
"companyName": "汇蕴",
"fxList": [
{
"title": "100万≤X<300万",
"price": "2.0%",
"isFloat": false,
"earningRate": "8.5%",
"packingRate": 8.5
},
{
"title": "300万≤X<1000万",
"price": "1.5%",
"isFloat": false,
"earningRate": "9%",
"packingRate": 9
},
{
"title": "1000万≤X",
"price": "1.0%",
"isFloat": false,
"earningRate": "9.5%",
"packingRate": 9.5
}
],
"sourceRepayment": "1、项目公司运营收入\n2、项目公司股东自有资金\n3、银行贷款资金置换",
"fundInvest": "用于佛(山)清(远)从(化)高速公路北段工程建设项目的建设,以期获得投资收益",
"windControl": "1、央企中电建路桥出具完工承诺,保证项目按期运营\n2、投资人成为融资主体股东",
"highlights": "?【省级重点】标的是广东省基建-高速公路建设,是从“十一五”就提出的建设规划,此项目为广东省重点工程,由政府牵头并给予大力支持,项目公司也与清远市交通运输局签订《特许经营协议》,项目违约成本极高; \n?【项目把控实力】中证基金是项目公司第一大股东,对整体项目有掌控权,有助于资金安全及还款的及时有效;同时一同参与的有中电建、龙浩集团;\n?【管理人背景】管理人由中证基金99%控股,中证基金股东为中信证券、华夏资本、中诚信托;\n?【央企增信】央企中电建路桥出具完工承诺,保证项目能够按期运营;\n?【银行资金】项目已有意向银行资金83.36亿贷款划拨计划,安全边际较高;",
"productOrganizationId": 318,
"productOrganizationPic": "Upload/productOrganization/20161012/logo_473.png",
"bqqsr": "2016-12-12 00:00:00",
"province": 440000,
"proviceName": "广东省",
"cityName": "佛山市",
"dyl": -1,
"addrId": null,
"updateTime": "2016-12-20 09:48:13",
"listingTime": "2016-12-12 11:41:00",
"parentId": 7300,
"phase": 6,
"issuerCompanyId": 0,
"issuerCompanyName": "",
"issuerId": 0,
"issuerPhone": "",
"issuerName": "",
"project": 3,
"attr": 7,
"jianBan": "",
"appointmentCount": 53,
"downloadCount": 385,
"sendEmailCount": 21,
"cashback": "",
"tagsArray": [
{
"id": "13",
"name": "固定收益类"
},
{
"id": "11",
"name": "二年期"
}
],
"bestEarningRate": "9.5%",
"bestEarningRate_fore": 9,
"bestEarningRate_back": "5",
"bestPrice": "2.0%",
"bestPrice_fore": 2,
"bestPrice_back": "0",
"bestGroupPrice": "待定",
"bestGroupPrice_fore": "待定",
"bestGroupPrice_back": 0,
"bestEmployeePrice": "待定",
"bestEmployeePrice_fore": "待定",
"bestEmployeePrice_back": 0,
"saleStatusName": "募集中",
"isHot": false,
"isHotSale": true,
"isRecommend": false,
"packingRate": 0,
"returnCash": 0
}
],
"isSuccess": true,
"code": 0,
"runSpanTime": 97
}
像上面这个demo数据一样。泥他妈这么多字段,让前端、APP都去理解?是否前端也要参与数据库设计?前后端分离的意义在哪?
sorry,忍不住吐槽。
问题4:安全,还是安全
安全这个方向的问题太多,很难一一排查,如见到此类问题就想把协议换为HTTPS的人来说,你基本是还不了解安全。对ROOT后的Android系统来说,HTTPS并没有这么神秘。API上必须考虑安全性。
- 查询ID是不是该用主键?
- 所有下发的字段显示是否应该直接下发?
- 登录限制,频率限制
- 加密措施,如何做?
- 请求路径将表内关系完全暴露,响应结果将表结构暴露,SQL注入\数据爬虫\Replay攻击 防范要求太高。
当然,RESTful API规则上还有很多关于过滤、错误码(我最不能接受的就是状态码,很多运营商直接都劫持了,把你坑死)的说明,这里我们就不一一列举了。
上述一系列的问题,让我们在整个系统开发的过程中,使用倍感困惑。也许是自己对RESTful的了解不够到位,或者使用上无法掌握其精髓。所以,我们最后在设计自己API的时候,采取的是类RESTful协议,用其思想,并在RESTful的基础是做了很多的自定义操作功能。
request设计
整个设计上借鉴了RESTful的思想,将操作同样分为CURD(增删改查),如对用户(User)进行操作
- sheme命名
user/create # 创建
user/delete # 删除
user/update # 更新
user/login # 登录
user/info # 用户信息
user/list # 列表
# 多表联查
getUserKpi?agemin=30&agemax=45 #kpi表和user表,根据需求来取名
- 参数过滤
user/list?query=周%&pageSize=10&start=0 #分页参数过滤
- 通用请求头部,自定义header
当然,最重要的是,我们的API都需要监控,根据版本号做兼容等,我们需要在request的header之中自定义一些信息,这里我们定义为json格式的信息。如下:
{
"userId": "1000",
"platform": "android",
"imei": "xxxxx",
"appVersion": "1.0",
"cityId": "0",
"platformVersion": "4.2",
"deviceId": "xxxx",
"channel": "xxx",
"protoVersion": 1
}
参数名称 | 类型 | 说明 |
---|---|---|
userId | int | 用户的id |
platform | string | 请求类型平台。目前有:iOS、Android、pcweb、h5 |
imei | string | 设备imei |
appVersion | string | 请求来源的version,这里通常指的是app的版本号 |
cityId | int | 定位到的城市id |
platformVersion | string | 改设备的版本号,如:Android4.4.4,、iOS10.0.2 |
deviceId | string | |
channel | string | 分发渠道标识 |
protoVersion | int | 协议版本号 |
response设计
相比request,response的设计规范相对简单很多(返回内容设计还是比较重要的,下一篇向大家介绍)。
还是根据RESTful的方式,我们将返回结果分为两部分,错误码和实际结果,如下所示:
{
"head": {
"ret": 0,
"msg": "ok",
"cmd": "user/info"
},
"body": {
"list": [
{
}
]
}
}
参数名称 | 类型 | 说明 |
---|---|---|
head | object | 通用信息 |
msg | string | 错误信息 |
cmd | string | 请求action |
body | object | 返回结果 |
如何设计body里面的结果才是response的关键,整个API数据解耦的难题。这里不做详细介绍了,下一篇给大家一一分享下。
有问题,或者想吐槽的,请加韬哥微信:
/*
* @author zhoushengtao(周圣韬)
* @since 2016年12月21日 凌晨0:53:13
* @weixin stchou_zst
* @blog http://blog.csdn.net/yzzst
/
用产品思维设计API(一)——RESTful就是个骗局