記得第一次寫APP的時候,那時還完全不知道REST
這個東西,對Web Service
也是一知半解。我和另一個同學在討論使用什麼協議來互動時,通過各自充分的調研之後(其實就是搜尋引擎找一找……),一致認為,HTTP
這個東西本身就對頻寬的消耗這麼大了,這麼多Web Service
(當時還是SOAP
當道)還是基於HTTP
之上的,這得浪費多少頻寬啊。最後一致決定使用Socket
來通訊,現在想想當時也是挺不容易的,我們硬是在Socket
上搭了一套通訊協議,還發展到了第二版。
今天在移動應用普及、前後端分離的大浪潮下,RESTful
風格的API大行其道,可是因為它本身就是一個比較模糊且寬泛的概念,所以每個人對它的理解都有千差萬別。我覺得我們在技術選型的時候,在自己的技術積累以及參考已有的行業最佳實踐的基礎上,應當首先考慮自身系統的需求,思考「選擇某一種技術」會對系統的開發和維護帶來哪些好處與壞處,而不是人云亦云看著別人用什麼自己就用什麼。而且RESTful API
設計目前並沒有一個公認的行業最佳實踐,故而開發者在設計一個API系統時,更應該根據自身的情況量身定製,千萬不要說「我照著某某公司的開放API照搬」就好了。 本文將根據我使用REST的經驗來總結一下RESTful API
設計的一些知識和經驗,自勉。本文將不討論Oauth
等安全問題。
首先理清一些概念:
REST
(Representational State Transfer)
定義了一套基於Web的資料互動方式的設計風格。RESTful
符合REST風格的API就可以叫做RESTful API。注意,本文講到的RESTful API設計方法將是基於HTTP和JSON實現方式,但不論HTTP還是JSON都不是REST的標準。REST只是風格,沒有標準。- 動詞、
RPC
在微信裡搜尋【RESTful API 設計】,出來好多文章都是說怎麼在RESTful Uri裡使用動詞等等,這些人除了一部分是把文章拿來抄一抄的,其他的其實搞混了REST和RPC的概念了,REST強調資源,RPC強調動作,所以REST的Uri組成為名詞,RPC多為動詞短語。然而它們也並不是完全不相關的兩個東西,本文中的實現就會參考一部分JSON-RPC
的設計思想。 - Web Service
這個是一個更古老的概念,有一套它的理論,不過我更傾向於把它理解成任何基於Web提供的服務。
設計方法及原則:
1. 使用HTTP方法:
HTTP1.1的規範定義了8個動詞,然而HTTP作為一個規範並沒有被嚴格地遵守著,在大多數情況下POST是可以完成除任何種類的請求,所以現在很多的API設計都是隻是用GET和POST來呼叫API,在這種情況下,一般的做法是使用GET用來獲取資源,其他的行為都是用POST來完成,而為了區別不同的行為,往往在API的Uri中加入動詞,如百度推送的如下API:
[
POST
] /rest/3.0/app/del_tag功能
刪除一個已存在的tag
引數
引數名 型別 必需 限制 描述 tag string 是 1~128位元組,但不能為‘default’ 標籤名稱 返回值
名稱 型別 描述 tag string 標籤名稱 result number 狀態 0:建立成功; 1:建立
更清晰API設計的可能會使用GET POST PUT DELETE四種方法分別代表“查詢、新增、更新、刪除”等四個動作,這在概念上是符合HTTP規範的,如Google的如下API:
Request
DELETE
https://www.googleapis.com/bigquery/v2/projects//datasets/?key={YOUR_API_KEY}Response
404 Not Found
– Show headers –
Not Found
在我看來,沒有絕對的好與不好。如果使用第一種方法,那麼只要保證Uri的語義清晰,其實和使用第二種方法沒有太大的區別。
2. Uri
格式:
Uri
在REST中標識了一個資源,但是在具體的API設計中,往往不能做到完全的對於資源的對映,本文中的設計將參考比較流行的Uri
設計,大致有這麼幾條:
Uri
的根(root
,/
)應當能夠標識這是一個RESTful API,以與同目錄下其他可能存在的資源進行區分。- 緊接著
Uri
的根,應當標識當前API的版本號。 - 如果方法是POST或者PUT,儘量避免使用URL編碼的引數,儘量保持Uri的乾淨。
- 如果方法是DELETE,Uri應當完全標識了需要刪除的物件或者物件的集合,避免在DELETE的請求中使用其他引數,因為某些伺服器可能會丟棄伴隨著DELETE傳送的內容。
這裡再次拿行業標杆Google的開放API來舉例:
POST
https://www.googleapis.com/books/v1/mylibrary/annotations
PUT
https://www.googleapis.com/bigquery/v2/projects/p1/datasets/p2
DELETE
https://www.googleapis.com/bigquery/v2/projects/{project-parameter}/datasets/{datasets-parameter}
3. 固定返回碼
REST
的大部分實現都是一個基於HTTP
的,那麼自然而然就少不了與返回碼打交道,然而不幸的是,HTTP
的返回碼定義的看起來十分隨意,很多錯誤資訊語焉不詳,而且在實際的開發中,API的使用者需要處理鏈路的問題(如超時等)、種類繁多的HTTP
返回碼、和實際的返回內容,不堪其繁瑣。更嚴重的是,這些返回碼大多最終依賴於服務端開發者的具體實現,而這種看似約定的東西分別在客戶端和服務端開發者眼中的含義可能相去甚遠。
那麼從需求入手,我們在使用RESTful API
時需要使用返回碼的原因大致是這樣的:客戶端在呼叫一個API之後,需要在接收到的反饋必須要能夠標識這次呼叫是否成功,如果不成功,客戶端需要拿到失敗的原因。我們可以在API設計時作一個小小的約定,就能完美的滿足以上需求了。
服務端在成功接收到客戶端的請求之後,永遠返回200,具體成功與否及進一步的資訊放入返回的內容。
在這個場景中,如果是鏈路出了問題或者伺服器錯誤等(返回碼不等於200
),客戶端很容易就能捕獲這個錯誤,如果鏈路沒問題,那麼出錯與否在獲取到的反饋內容中會有詳細的描述。
4. 固定返回結構
現在越來越多的API設計會使用JSON來傳遞資料,本文中的設計也將使用JSON。JSON-RPC
是一個基於JSON的廣為人知的設計簡潔的RPC規範,本文將借鑑JSON-RPC
的響應物件的設計。
JSON-RPC中服務端響應物件的設計的基本理念是,只要呼叫成功,服務端必須響應資料(如在#3中討論的那樣),而響應資料的格式在任何情況下都應當是一致的,JSON-RPC的響應格式是這麼設計的:
1 2 3 4 5 6 7 8 9 10 11 |
{"jsonrpc": "2.0", "result": 19, "id": 1} { "jsonrpc": "2.0", "error": { "code": -32600, "message": "Invalid Request" }, "id": null } |
jsonrpc
A String specifying the version of the JSON-RPC protocol.
MUST
be exactly “2.0”.result
This member is
REQUIRED
on success.This member
MUST NOT
exist if there was an error invoking the method.The value of this member is determined by the method invoked on the Server.
error
This member is
REQUIRED
on error.
This memberMUST NOT
exist if there was no error triggered during invocation.The value for this member
MUST
be an Object as defined in section 5.1.id
This member is
REQUIRED
.It
MUST
be the same as the value of the id member in the Request Object.If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it
MUST
be Null.
由於JSON-RPC
的目標是建立一個通用的規範,所以響應格式的設計還是有些複雜,我們可以只取其中它對於error
物件的設計,所有返回的格式必須是這樣的:
1 2 3 4 5 |
{ "code": -32600, "message": "Invalid Request”, “data”:{ } } |
這種格式的設計在許多大公司的開放API中也較為常見,比如作為行業標杆的Google,在呼叫Google開放平臺的某API後獲取到的錯誤資料如下,其設計思想與本文討論的這種返回格式的思想如出一轍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{"error": { "errors": [ { "domain": "global", "reason": "required", "message": "Login Required", "locationType": "header", "location": "Authorization" } ], "code": 401, "message": "Login Required" } } |
綜上所述,本文所探討的API設計是這樣的:
- 所有API的Uri為基於HTTP的名詞性短語,用來代表一種資源。
- Uri格式如文中所述。
- 使用GET POST PUT DELETE四種方法分別代表對資源的“查詢、新增、更新、刪除”。
- 服務端接收到客戶端的請求之後,統一返回200,如果客戶端獲取到的返回碼不是200,代表鏈路上某一個環節出了問題。
- 服務端所有的響應格式為:
12345{“code”: -32600,“message”: “Invalid Request”,“data”:{ }}
他們的含義分別代表:- code為0代表呼叫成功,其他會自定義的錯誤碼;
- message表示在API呼叫失敗的情況下詳細的錯誤資訊,這個資訊可以由客戶端直接呈現給使用者,否則為空;
- data表示服務端返回的資料,具體格式由服務端自定義,API呼叫錯誤為空