大家好,我是 17 。
RESTful 是目前最流行的 API 设计规范,用于 Web 数据接口的设计。但是实操中会遇到很多问题。
1. 服务器不支持 PUT,DELETE 方法
有人说加一个头
POST /api/Person/4 HTTP/1.1 X-HTTP-Method-Override: PUT 复制代码
但实际上,不是加上这个头,服务器就自动支持了,还得进行配置才行。
2. 大家对规范的理解不大一样
比如版本号,有人说直接放 path 里,有人说放参数里,等等。
规范的意义之一是减少沟通成本,既然无法统一,也不大可能通用,那在自己的团队里还不如自己制定一个规范,用着舒服就行了。当然了,自己制定规范的时候,可以参考 RESTful。
通用接口规范
路径一律用单数名词,最多两级路径
路径代表资源的位置。既然是位置,当然没有单数复数之分。最多两级是为了简化逻辑。
/article /fruit /fruit/apple 复制代码
动作用 method 参数
每个位置的资源都可以进行各种处理。
method 可以是 http 协议中规定的动作: GET、POST、PUT、DELETE,HEAD。虽然还有其它动作,但是为了简化,我们只用这 5 个动作。服务端在执行这些动作时必须要与 http 协议中对这 5 个动作的规定相符。
其它参数
其它用来描述资源的都用参数来表示
/fruit?price=1&color=red 复制代码
命名空间
对于不同的项目,api 的 path 可能会冲突。如果一个 api 需要多个项目通用,可以在最前面多加一级加上命名空间。
/n-market/fruit/apple 复制代码
n-market 就是命名空间,为了便于和后面的 path 区分 以 n- 开头。
实现规范
在实现层面有非常多的选择,不方便给出适合所有情况的方案。但为了让大家了解本规范,还是决定给出一个供参考的方案。
转发不同命名空间的请求
既然要用一个独立的命名空间,这些 api 应该是一个独立的服务。nginx 直接对请求进行转发。
location /n-market/ { proxy_pass http://127.0.0.1:3001/; } 复制代码
- 把 /n-market/ 开头的请求转发到 3001 端口,并去掉 /n-market/。
比如 请求 /n-market/a/b
3001 接口接收到的请求 为 /a/b
对于服务内部来说,/n-market/ 并不需要,/n-market/ 只是在服务外面用的,用来和其它服务的接口做区分。
除了 get,head,其它的都用 post
一般来说,服务支持 HEAD,GET,POST 三种 method。对于服务器可能不支持的 PUT,DELETE,在客户端直接发送 post 请求。拿 js 举例
let httpMethod = ''; switch (method) { case 'HEAD': case 'GET': httpMethod = method; break; default: httpMethod = 'POST' break; } 复制代码
服务端根据 path 做处理
路径最多有两级,服务端也可以设置两级处理。比如第一级对应一个类,第二级对应一个方法。方法里处理具体的动作。这里有动作有四种,是通过 url 参数传过来的。 head 一般在 nginx 就直接处理了,所以这里只有四种方法。
class Fruite{ apple() { switch (method) { case 'GET': case 'GET': case 'PUT': case 'DELETE': } } banana() { } } 复制代码
如果路径只有一级,相当于省略后面的方法名,默认的方法名和类同名。比如 /fruite
等价于 /fruite/fruite
所以我们可以用统一的方式处理路径,通过路径找到相应的类和方法做处理。
GET
读取资源。
DELETE
删除资源,具有幂等性。无论删除多少次,产生的影响都是一样的。本来这块应该是 nginx 处理的,但既然是服务端处理,相关的状态码应该和规范一致。
如果 DELETE method 已经成功处理,服务器应返回如下状态码
- 202 (Accepted) 请求已经被接受,不出意外会成功,但是目前还没执行。
- 204 (No Content) 已执行删除,但没有发现内容。
- 200 (OK) 成功执行删除
默认用 id 做为标识资源唯一性的 key
如果一个资源可以用一个key 找到,这个 key的名字应该是 id。作了这个约定后,可以大大降低沟通成本。 如果一次处理多个 id,id 可以重复。
比如
/fruite/apple?id=1&id=2 复制代码
用 v 表示 版本
我们规定:接口的版本号用参数 v 表示。版本号采用三位表示法。
- 第一位:大版本,颠覆性的变化
- 第二位:小版本,功能变化
- 第三位: 修复版本。用来 fix bug 等小调整。
比如
/fruite/apple?v=1.0.0 /fruite/apple?v=2.1.1 复制代码
默认为最新版本,可以省略 v 不写。
最小版本号为 0.1.0
。
PUT
用来创建替换资源。具有幂等性。
- 201 (Created)] 创建成功。
- 200 (OK)) 替换成功。规范说,返回 204 也可以,我们规定统一返回 200。
POST
如果不适合用前面三种动作,就是 POST。发送的内容可以用三种格式
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
签权
对于危险的动作是应该有限制的,只有授权的用户才可以操作。我们规定签权信息放 header 里。可以放 cookie 或 自定义的头 token 里面。如果是放 cookie里面,key值必须为 token。
为什么还需要 cookie ,统一用 header 不好吗?
- 浏览器中用 cookie会更加方便。发请求的时候自动带上。
- 在浏览器可以设置 cookie 为
HttpOnly
,让 js 无法读取,可以用Secure
采用安全通道, 更加安全。
所以在客户端为浏览器的时候还是用 cookie 为好。
案例
我们经常遇到这样的场景,增删改查。如果现在要设计一个 todo list 的 增删改查 的 api ,如何设计呢?
如果不按规范设计,可能得需要 8 个 api。比如 /createItem
,/modifyItem
...
用规范来设计只需要一个api 就够了。
描述 | method | path | 参数 |
创建 item | POST | /item | 无 |
修改 item | PUT | /item | id |
查看所有 item | GET | /item | 无 |
查看某个 item | GET | /item | id |
删除所有 item | DELETE | /item | 无 |
删除 item | DELETE | /item | id |
创建月总结 item | PUT | /item | month |
如果是删除多个 id 可以这样写 id=1&id=2&id=6
创建 item 用 POST 是因为多次执行会创建多个。
用 PUT 要求多次执行产生的影响一样。月总结每个月只有一个,多次执行也只有一个结果,所以用 PUT。
通过 method,和几个参数的配合,一个接口就完成同一个资源所有操作。这样清晰,也容易沟通,开发效率会大大提高。只要知道资源的名字,就知道如何处理资源。
又比如有一个水果店的项目,有一些苹果,定义苹果的 path /fruite/apple
,只需要很少的沟通,前端和服务端立即就知道接口如何写。因为我们规定 id 作为唯一性的 key,用 POST、PUT、DELETE、GET 作为动作,只要明确辅助参数,很容易就能唯一确定接口的写法。
对于创建和删除这样的危险动作肯定是需要权限的。我们前面规定把签权信息放在 header 里。由对应的类和方法做实际的权限验证。