## 假的 REST 接口
很多人看到 REST 反应就是,利用 http 动词,处理资源, 随便看看就明白了。
这种人很容易就写出这样一种接口,所有的请求统一返回 200,body 中有,success, message, data, 大家的实现不同,但是大致就是这么个意思。
随处也都能看到这样的讨论,比如[这里](https://www.google.co.jp/search?q=site:v2ex.com/t+rest&gws_rd=cr&ei=eoiaWMypLcej8QW7uI5g)。
## 理解 REST
我们以一个简单的例子开始,假设我们需要写一套api,功能包括了用户,商品,订单,供应商。
### 资源
写 REST 接口,首先需要明白`资源`这个概念,所有的东西都是资源,我们始终是在操作资源,当然资源需要是个名词。
于是我们定义出这样一些资源,users, products, orders, vendors。注意这里是复数,因为既然是资源,肯定是一堆,是个集合。单复数的概念还是很重要的。
### 状态码
状态码是很重要的,是有意义的,客户端是需要根据状态码做判断的。比如201表示资源被创建了;204 表示请求成功了,但是并没有什么信息需要返回,body 当然是空;202 表示服务器接受了请求,但是还未处理,响应中应该包含相应的指示信息,告诉客户端该去哪里查询这次处理是否真正完成了等等。
你可能会遇到一些同事说它判断不了状态码,一定要解析body,body中需要一个字段表示是否成功...等等,那么你需要做的是跟他讲道理,让他看看 http 协议。
有用的链接 [https://httpstatuses.com/](https://httpstatuses.com/)
### 处理资源
有了资源,我们就会需要对资源进行增删改查,对应到 http 的动词,就是 post,delete,put/patch,get。
以一个商品资源为例
| http 动词 | url | 返回状态码 | 描述 |
| --- | --- | --- | --- |
| post | /api/products | 201 | 创建一个商品 |
| get | /api/products | 200 | 获取商品列表 |
| get | /api/products/{id} | 200 | 获取某个商品信息 |
| put | /api/products/{id} | 200 / 204 | 完整的替换某个商品 |
| patch | /api/products/{id} | 200 / 204 | 部分更新某个商品 |
| delete | /api/products/{id} | 204 | 删除某个商品 |
可参考 [rfc7231](https://tools.ietf.org/html/rfc7231)
### 第一个问题
用户,商品,订单,供应商 每个都是独立的资源,那么如果我有一个订单详情页面,显示哪个用户买的什么商品,买的谁的商品,什么时间买的。也就是同时获取了4个资源的信息,难道要请求4次吗?
比如订单id为10,用户id为1,商品id为5,供应商id为2。
* get /api/orders/10
* get /api/products/5
* get /api/vendors/2
* get /api/users/1
或者是另一种解决办法,订单详情默认包含了它的商品,用户,供应商信息?
* get /api/orders/10
这样是请求了一次,但是如果客户端只需要订单信息,我为什么要进行额外的查询,返回额外的信息。
**完美的方案**
首先,接口的设计应该站在资源的角度,关心的不是页面如何显示,而是客户端需要什么资源,而需要什么当然只能是客户端自己决定。其次资源之间是有关联的,我们要利用资源之间的关系,于是可能是下面这样:
~~~
get /api/orders/10?include=user,vendor,product
~~~
意思就是我需要10号订单的数据,同时需要订单相关的用户,供应商,商品信息。注意这里是单数,表示其相关的单个资源。
### 数据格式
有了上面完美的方案,但是资源的数据到底是什么样的,又要怎么嵌套呢?先参考一下[这里](http://jsonapi.org.cn/format/)。
对于资源来说,肯定需要一个统一的结构,也方便我们嵌套。我们先理解一个简单好用的json结构。
~~~
{
"data": {...}
"meta": {...}
}
~~~
data 中是这个资源的数据,meta 可选,是资源之外,其他的一些信息,比如分页。对于嵌套的资源同样也是这样。那么对于上面的请求,响应应该是下面这样。
~~~
get /api/orders/10?include=user,vendor,product
{
"data": {
"id": 10,
"title": 一个订单,
...
"product": {
"data": {
"id": 5,
"price": 15
...
}
}
"user": {
"data": {
"id": 1,
"name": "foo"
...
}
}
"vendor": {
"data": {
"id": 2,
"name": "bar"
...
}
}
}
}
~~~
再举个例子
~~~
我的订单列表,
get /api/user/orders?include=product,vendor
{
"data": [
{
"id": 1,
"title": 一个订单,
...
"product": {
"data": {
"id": 5,
"price": 15
...
}
}
"vendor": {
"data": {
"id": 2,
"name": "bar"
...
}
}
},
...
],
"meta": {
"pagination": {
"total": 60,
"count": 15,
"per_page": 15,
"current_page": 1,
"total_pages": 4,
"links": {
"next": "http://foobar/api/user/orders?page=2"
}
}
}
}
~~~
利用好资源的关系和嵌套我们再补充几个接口
| 动词 | url | 描述 | includes |
| --- | --- | --- | --- |
| get | /api/vendors/{id}/products | 获取某个供应商的所有商品 | vendor |
| get | /api/vendors/{id}/products/{id} | 获取某个供应商的某个商品 | vendor |
| get | /api/vendors | 获取供应商列表 | products |
| get | /api/user/orders | 我的订单列表 | vendor,products |
| get | /api/users/{id}/orders | 某个用户的订单列表 | vendor,products |
注意最后两个,这里是参考了 github,用单数的 user 表示当前用户,因为我们如果有token,服务器就知道我们是谁。
当然这里只是个例子,真实业务我们可能并不能查看别人下的订单,
### 第二个问题
我们下了一个订单,可能会伴随很多状态,待付款,已付款,已发货,已收货,已取消等等,如何设计api呢?
其实一开始大家很可能会这么写
~~~
patch /api/orders/{id}/pay 对某个订单付款
patch /api/orders/{id}/cancel 取消某个订单
~~~
这样或许可以,但是我们引入了动词,有没有更好的方法呢?
其实我们始终是在更新订单状态
~~~
patch /api/orders/{id}
body status: paid,canceled
~~~
或许可以这样,对于资源来说我们就是要把订单的状态改为paid或者canceled。但是对于每个状态,提交的参数和要处理的数据可能有很大的不同,难道都写在一个方法里?
~~~
$status = $request->get('status');
$method = camel_case('patch_'.$status);
return $this->$method($order);
~~~
接口是一个,但是我们接受到请求只有依然是可以进行接口分发的,类似上面这样。
### put 和 patch 的关系
两个方法都是更新资源,而且幂等的,但是 put 是整个替换资源,首先需要判断必填项,然后根据请求替换这个资源。patch 是提交什么更新什么。
第二 put 是可以创建资源的,但是一般只存在于客户端可以指定资源id的情况下
~~~
put /api/orders/100
~~~
更新资源id为100的资源,如果不存在则创建。创建的话返回201,更新的话返回200。这种情况很少见,因为现在基本上都是服务器生成id。所以对于我们平时处理的业务,其实大部分是 patch。
### 版本区分
有了 api 当然需要区分版本,因为使用时需要更新的。
那么用什么来区分版本其实大体上有两种
~~~
/api/v1/orders
/api/v2/orders
~~~
或者利用 header
~~~
/api/orders
Accept: application/vnd.foobar.v1+json
Accept: application/vnd.foobar.v2+json
~~~
一些教程里面觉得放在url上更直观,比如阮一峰的教程,很多人也用 github 作为例子。
但是其实你看看 github api 的第一页,[https://developer.github.com/v3/](https://developer.github.com/v3/), github 及 其他一些的 rest 教程都是推荐第二种的。
## 总结
上面是我的个人理解,目前基本是按照这个思路实现的接口,但是依然也有很多地方觉得不完美,不规范。欢迎指正和讨论
- 关于我
- nginx
- 代理
- 实例1
- 使用Nginx实现反向代理
- nginx反向代理配置
- Nginx缓存原理及配置
- nginx 信号集
- rewrite
- nginx rewrite规则
- nginx rewrite 正则规则详解
- nginx+php-fpm fastcgi防止跨站、跨目录的安全设置
- 写了个shell脚本,实现nginx日志自动切割,并利用goaccess分析成HTML报表,然后通知管理人员查阅
- nginx禁止一些常见的不应该让用户访问的文件
- PHP
- 实现PHP多进程管理
- php代码规范
- Linux
- 常用命令
- scp
- touch
- nohup
- rsync
- ssh命令 远程登录
- chmod
- nginx日志切割脚本
- Linux使用ssh公钥实现免密码登录Linux
- 使用ssh公钥密钥自动登陆linux服务器
- Linux查看程序端口占用情况
- ssh 公钥私钥认证原理
- 路由图
- CentOS7防火墙设置
- linux下用户管理
- Linux 服务器安全技巧
- Linux shell 提取文件名和目录名
- Linux环境下设置命令别名(alias)
- Git
- 搭建Git服务器
- 手把手教你搭建git服务器
- Git 基本操作
- git删除文件夹/文件(不删除本地文件)
- git忽略文件和文件夹以及文件权限
- Git的四个区五种状态
- git init 和 git init --bare 的区别
- git仓库删除所有提交历史记录,成为一个干净的新仓库
- 如何正确使用Git Flow
- HTTP
- 前端跨域解决方案
- HTTP基本概念
- Redis
- Redis 数据类型及应用场景
- JavaScript
- WebSocket探秘
- socket与websocket的区别
- 基于 Swoole 的微信扫码登录
- Web前端开发规范手册
- 简单的学习了一下ES6
- ES6模块的import和export用法
- MySql
- MySql重置root密码的方法
- mysql 5.6.30 开启慢查询日志
- mysql查看锁表情况
- 我的设计稿
- 会计出纳系统
- 上门宝ERP系统
- 社交应用七大需求
- 出纳系统
- 利用SdfMpp搭建中大行软件系统
- B2C库存商品系统
- 软件
- 网上商城
- 思维逻辑
- 系统架构分层图
- 代码检查规范
- 研发计划
- 系统架构
- 我设计的架构架构图
- 电商网站架构案例
- 大型网站架构
- 服务熔断、降级、限流、异步RPC
- 软件版本号规范
- 第三方服务引擎
- kafka
- ElasticSearch
- 阿里云 推荐引擎
- 随记
- 人生最大的投资
- 产品研发团队管理的6个方面
- 我感觉自己根本不是一个创业者,更谈不上企业家,果然还是屌丝这个称呼更适合我。
- 学什么语音都应该养成的编程习惯
- 2018年5月23日 多云转阴 15-24°C 离职信
- 2017年12月25日
- 对“目标”的认知
- 个人简历
- API设计
- 理解OAuth 2.0
- OAuth 2.0四种授权方式小结
- 对 REST 的理解
- RESTful API 设计指南
- RESTful API 设计
- RPC
- 微服务架构方案
- SOA
- 理解RESTful架构
- RESTful API GET,DELETE,PUT和POST的响应状态码
- MongoDB
- MongoDB-无法启动的一个问题