對 REST 的理解

liyu001989發表於2017-02-10

整理一下目前為止我對 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 協議》,轉載必須註明作者和本文連結

相關文章