整理一下目前為止我對 REST 的理解
假的 REST 介面
很多人看到 REST 反應就是,利用 http 動詞,處理資源, 隨便看看就明白了。
這種人很容易就寫出這樣一種介面,所有的請求統一返回 200,body 中有,success, message, data, 大家的實現不同,但是大致就是這麼個意思。
隨處也都能看到這樣的討論,比如這裡。
理解 REST
我們以一個簡單的例子開始,假設我們需要寫一套api,功能包括了使用者,商品,訂單,供應商。
資源
寫 REST 介面,首先需要明白資源
這個概念,所有的東西都是資源,我們始終是在操作資源,當然資源需要是個名詞。
於是我們定義出這樣一些資源,users, products, orders, vendors。注意這裡是複數,因為既然是資源,肯定是一堆,是個集合。單複數的概念還是很重要的。
狀態碼
狀態碼是很重要的,是有意義的,客戶端是需要根據狀態碼做判斷的。比如201表示資源被建立了;204 表示請求成功了,但是並沒有什麼資訊需要返回,body 當然是空;202 表示伺服器接受了請求,但是還未處理,響應中應該包含相應的指示資訊,告訴客戶端該去哪裡查詢這次處理是否真正完成了等等。
你可能會遇到一些同事說它判斷不了狀態碼,一定要解析body,body中需要一個欄位表示是否成功...等等,那麼你需要做的是跟他講道理,讓他看看 http 協議。
有用的連結 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
第一個問題
使用者,商品,訂單,供應商 每個都是獨立的資源,那麼如果我有一個訂單詳情頁面,顯示哪個使用者買的什麼商品,買的誰的商品,什麼時間買的。也就是同時獲取了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號訂單的資料,同時需要訂單相關的使用者,供應商,商品資訊。注意這裡是單數,表示其相關的單個資源。
資料格式
有了上面完美的方案,但是資源的資料到底是什麼樣的,又要怎麼巢狀呢?先參考一下這裡。
對於資源來說,肯定需要一個統一的結構,也方便我們巢狀。我們先理解一個簡單好用的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/, github 及 其他一些的 rest 教程都是推薦第二種的。
總結
上面是我的個人理解,目前基本是按照這個思路實現的介面,但是依然也有很多地方覺得不完美,不規範。歡迎指正和討論
本作品採用《CC 協議》,轉載必須註明作者和本文連結