我所認爲的RESTful API最佳實踐

我所認爲的RESTful API最佳實踐

不要糾結於無心義的規範

在開始本文以前,我想先說這麼一句: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 將它們用於提交應用市場的版本號傳遞到服務器,而是提供相似於v1v2之類的 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,說明增量的數據太多因此只返回了第一頁,須要清空舊的列表。

相關文章
相關標籤/搜索