首页 > 代码库 > 用产品思维设计API(一)——RESTful就是个骗局

用产品思维设计API(一)——RESTful就是个骗局

用产品思维设计API(一)——RESTful就是个骗局

前言

最近公司内部在重构项目代码,包括API方向的重构,期间遇到了很多的问题,不由得让我重新思考了下。
- 一个优雅的API该如何设计?
- 前后端分离之后,API真的解耦分离了吗?
- 不断的版本迭代,API的兼容性该如何做?

年前,我司内部的接口已经进入了一个完全的重构阶段,参考了市面上各大平台的API和文档,自己也总结出了很多的心得。这里向大家分享一下,接下来一个月,我们向从下面几个方面向大家介绍一个优雅的API(至少我认为挺优雅)该如何设计。

  1. RESTful就是个骗局
  2. 数据解耦,才是前后分离的本质
  3. 版本控制,没有你想的这么简单
  4. 随意定义错误码,你还在这样干?
  5. 安全,就只能用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
/
技术分享

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    用产品思维设计API(一)——RESTful就是个骗局