不要纠结于无心义的规范
在开始本文以前,我想先说这么一句:RESTful 真的很好,但它只是一种软件架构风格,过分纠结如何遵照规范只是徒增烦恼,也违背了使用它的初衷。数据库
就像 Elasticsearch 的 API 会在 GET 请求中直接传 JSON,但这是它的业务须要,由于普通的 Query Param 根本没法构造如此复杂的查询 DSL。Github 的 V3 API 中也有不少不符合标准的地方,这也并不会妨碍它成为业界 RESTful API 的参考标准。后端
我接下来要介绍的一些东西也会跟标准不符,但这是我在实际开发中遇到过、困扰过、思考过所得出的结论,因此才是我所认为的
RESTful API 最佳实践。api
为何要用 RESTful
RESTful 给个人最大感受就是规范、易懂和优雅,一个结构清晰、易于理解的 API 彻底能够省去许多无心义的沟通和文档。而且 RESTful 如今愈来愈流行,也有愈来愈多优秀的周边工具(例如文档工具 Swagger)。缓存
协议
若是能全站 HTTPS 固然是最好的,不能的话也请尽可能将登陆、注册等涉及密码的接口使用 HTTPS。服务器
版本
API 的版本号和客户端 APP 的版本号是毫无关系的,不要让 APP 将它们用于提交应用市场的版本号传递到服务器,而是提供相似于v1
、v2
之类的 API 版本号。版本号只容许枚举,不容许判断区间。网络
版本号拼接在 URL 中或是放在 Header 中均可以。例如:架构
api.xxx.com/v1/users
或:app
api.xxx.com/users version=v1
请求
通常来讲 API 的外在形式无非就是增删改查(固然具体的业务逻辑确定要复杂得多),而查询又分为详情和列表两种,在 RESTful 中这就至关于通用的模板。例如针对文章(Article)设计 API,那么最基础的 URL 就是这几种:工具
GET /articles
: 文章列表GET /articles/id
:文章详情POST /articles/
: 建立文章PUT /articles/id
:修改文章DELETE /articles/id
:删除文章
RESTful 中使用 GET、POST、PUT 和 DELETE 来表示资源的查询、建立、更改、删除,而且除了 POST 其余三种请求都具有幂等性(屡次请求的效果相同)。须要注意的是 POST 和 PUT 最大的区别就是幂等性,因此 PUT 也能够用于建立操做,只要在建立前就能够肯定资源的 id。布局
将 id 放在 URL 中而不是 Query Param 的其中一个好处是能够表示资源之间的层级关系,例如文章下面会有评论(Comment)和点赞(Like),这两项资源必然会属于一篇文章,因此它们的 URL 应该是这样的:
评论:
GET /articles/aid/comments
: 某篇文章的评论列表GET /comments/cid
: 获取POST /articles/aid/comments
: 在某篇文章中建立评论PUT /comments/cid
: 修改评论-
DELETE /comments/cid
: 删除评论这里有一点比较特殊,永远去使用能够指向资源的的最短 URL 路径,也就是说既然
/comments/cid
已经能够指向一条评论了,就不须要再用/articles/aid/comments/cid
特地的指出所属文章了。点赞:
-
GET /articles/id/like
:查看文章是否被点赞 PUT /articles/id/like
:点赞文章DELETE /articles/id/like
:取消点赞
RESTful 中不建议出现动词,因此能够将这种关系做为资源来映射。而且因为大部分的关系查询都与当前的登陆用户有关,因此也能够直接在关系所属的资源中返回关系状态。例如点赞状态就能够直接在获取文章详情时返回。注意这里我选择了 PUT 而不是 POST,由于我以为点赞这种行为应该是幂等的,屡次操做的结果应该相同。
Token 和 Sign
API 须要设计成无状态,因此客户端在每次请求时都须要提供有效的 Token 和 Sign,在我看来它们的用途分别是:
- Token 用于证实请求所属的用户,通常都是服务端在登陆后随机生成一段字符串(UUID)和登陆用户进行绑定,再将其返回给客户端。Token 的状态保持通常有两种方式实现:一种是在用户每次操做都会延长或重置 TOKEN 的生存时间(相似于缓存的机制),另外一种是 Token 的生存时间固定不变,可是同时返回一个刷新用的 Token,当 Token 过时时能够将其刷新而不是从新登陆。
- Sign 用于证实该次请求合理,因此通常客户端会把请求参数拼接后并加密做为 Sign 传给服务端,这样即便被抓包了,对方只修改参数而没法生成对应的 Sign 也会被服务端识破。固然也能够将时间戳、请求地址和 Token 也混入 Sign,这样 Sign 也拥有了所属人、时效性和目的地。
统计性参数
我不太清楚这类参数具体该被称为何,总之就是用户的各类隐私【误。相似于经纬度、手机系统、型号、IMEI、网络状态、客户端版本、渠道等,这些参数会常常收集而后用做运营、统计等平台,可是在大部分状况下他们是与业务无关的。这类参数变化不频繁的能够在登陆时提交,变化比较频繁的能够用轮训或是在其余请求中附加提交。
业务参数
在 RESTful 的标准中,PUT 和 PATCH 均可以用于修改操做,它们的区别是 PUT 须要提交整个对象,而 PATCH 只须要提交修改的信息。可是在我看来实际应用中不须要这么麻烦,因此我一概使用 PUT,而且只提交修改的信息。
另外一个问题是在 POST 建立对象时,究竟该用表单提交更好些仍是用 JSON 提交更好些。其实二者均可以,在我看来它们惟一的区别是 JSON 能够比较方便的表示更为复杂的结构(有嵌套对象)。另外不管使用哪一种,请保持统一,不要二者混用。
还有一个建议是最好将过滤、分页和排序的相关信息全权交给客户端,包括过滤条件、页数或是游标、每页的数量、排序方式、升降序等,这样可使 API 更加灵活。可是对于过滤条件、排序方式等,不须要支持全部方式,只须要支持目前用得上的和之后可能会用上的方式便可,并经过字符串枚举解析,这样可见性要更好些。例如:
搜索,客户端只提供关键词,具体搜索的字段,和搜索方式(前缀、全文、精确)由服务端决定:
/users/?query=ScienJus
过滤,只须要对已有的状况进行支持:
/users/?gender=1
对于某些特定且复杂的业务逻辑,不要试图让客户端用复杂的查询参数表示,而是在 URL 使用别名:
/users/recommend
分页:
/users/?offset=10&limit=10 /articles/?cursor=2015-01-01 15:20:30&limit=10 /users/?page=2&pre_page=20
排序,只须要对已有的状况进行支持:
/articles/sort=-create_date
PS:我很喜欢这种在字段名前面加-
表示降序排列的方式。
响应
尽可能使用 HTTP 状态码,经常使用的有:
- 200:请求成功
- 201:建立、修改为功
- 204:删除成功
- 400:参数错误
- 401:未登陆
- 403:禁止访问
- 404:未找到
-
500:系统错误
可是有些时候仅仅使用 HTTP 状态码没有办法明确的表达错误信息,因此我倾向于在里面再包一层自定义的返回码,例如:
成功时:
{ "code": 100, "msg": "成功", "data": {} }
失败时:
{ "code": -1000, "msg": "用户名或密码错误" }
data
是真正须要返回的数据,而且只会在请求成功时才存在,msg
只用在开发环境,而且只为了开发人员识别。客户端逻辑只容许识别code
,而且不容许直接将msg
的内容展现给用户。若是这个错误很复杂,没法使用一段话描述清楚,也能够在添加一个doc
字段,包含指向该错误的文档的连接。
返回数据
JSON 比 XML 可视化更好,也更加节约流量,因此尽可能不要使用 XML。
建立和修改操做成功后,须要返回该资源的所有信息。
返回数据不要和客户端界面强耦合,不要在设计 API 时就考虑少查询一张关联表或是少查询 / 返回几个字段能带来多大的性能提高。而且必定要以资源为单位,即便客户端一个页面须要展现多个资源,也不要在一个接口中所有返回,而是让客户端分别请求多个接口。
最好将返回数据进行加密和压缩,尤为是压缩在移动应用中仍是比较重要的。
分页
在 APP 后端分页设计 中提到过,分页布局通常分为两种,一种是在 Web 端比较常见的有底部分页栏的电梯式分页,另外一种是在 APP 中比较常见的上拉加载更多的流式分页。这两种分页的 API 到底该如何设计呢?
电梯式分页须要提供page
(页数)和pre_page
(每页的数量)。例如:
/users/?page=2&pre_page=20
而服务端则须要额外返回total_count
(总记录数),以及可选的当前页数、每页的数量(这两个与客户端提交的相同)、总页数、是否有下一页、是否有上一页(这三个均可以经过总记录数计算出)。例如:
{ "pagination": { "previous": 1, "next": 3, "current": 2, "per_page": 20, "total": 200, "pages": 10 }, "data": {} }
流式布局也彻底可使用这种方式,而且不须要查询总记录数(好处是减小一次数据库操做,坏处时客户端须要多请求一次才能判断是否到最后一页)。可是会出现数据重复和缺失的状况,因此更推荐使用游标分页。
游标分页须要提供cursor
(下一页的起点游标) 和limit
(数量) 参数。例如:
/articles/?cursor=2015-01-01 15:20:30&limit=10
若是文章列表默认是以建立时间为倒序排列的,那么cursor
就是当前列表最后一条的建立时间(第一页为当前时间)。
服务端须要返回的数据也很简单,只须要以此游标为起点的总记录数和下一个起点游标就能够了。例如:
{ "pagination": { "next": "2015-01-01 12:20:30", "limit": 10, "total": 100, }, "data": {} }
若是total
小于limit
,就说明已经没有数据了。
流式布局的分页 API 还有一种状况很常见,就是下拉刷新的增量更新。它的业务逻辑正好和游标分页相反,可是参数基本同样:
/articles/?cursor=2015-01-01 15:20:30&limit=20
返回数据有两种可能,一种是增量更新的数据小于指定的数量,就直接将所有数据返回(这个数量能够设置的相对大一些),客户端会将这些增量更新的数据添加在已有列表的顶部。可是若是增量更新的数据要大于指定的数量,就会只返回最新的 n 条数据做为第一页,这时候客户端须要清空以前的列表。例如:
{ "pagination": { "limit": 20, "total": 100, }, "data": {} }
若是total
大于limit
,说明增量的数据太多因此只返回了第一页,须要清空旧的列表。