關於 RESTful API 設計的總結

JeffreyC發表於2017-12-10

為什麼要用 RESTful

RESTful 給我的最大感覺就是規範、易懂和優雅,一個結構清晰、易於理解的 API 完全可以省去許多無意義的溝通和文件。並且 RESTful 現在越來越流行,

在開始介紹 RESTful API 之前,先講一下 RESTful 架構。

RESTful 架構

REST,即Representational State Transfer 的縮寫。意為 " 表現層狀態轉化 " 。

要理解RESTful架構,最好的方法就是去理解 Representational State Transfer 這個片語到底是什麼意思,它的每一個詞代表了什麼涵義。如果把這個名稱搞懂了,也就不難體會 REST 是一種什麼樣的設計。

資源 (Resources)

REST的名稱"表現層狀態轉化"中,省略了主語。"表現層"其實指的是"資源"(Resources)的"表現層"。

所謂"資源",就是網路上的一個實體,或者說是網路上的一個具體資訊。它可以是一段文字、一張圖片、一首歌曲、一種服務,總之就是一個具體的實在。你可以用一個 URI(統一資源定位符)指向它,每種資源對應一個特定的 URI 。要獲取這個資源,訪問它的 URI 就可以,因此 URI 就成了每一個資源的地址或獨一無二的識別符。所謂"上網",就是與網際網路上一系列的"資源"互動,呼叫它的 URI 。

表現層(Representation)

"資源"是一種資訊實體,它可以有多種外在表現形式。我們把"資源"具體呈現出來的形式,叫做它的"表現層"(Representation)。
比如,文字可以用 txt 格式表現,也可以用 HTML 格式、 XML 格式、JSON 格式表現,甚至可以採用二進位制格式;圖片可以用 JPG 格式表現,也可以用 PNG 格式表現。
URI 只代表資源的實體,不代表它的形式。嚴格地說,有些網址最後的" .html "字尾名是不必要的,因為這個字尾名錶示格式,屬於"表現層"範疇,而 URI 應該只代表"資源"的位置。它的具體表現形式,應該在 HTTP 請求的頭資訊中用 Accept 和 Content-Type 欄位指定,這兩個欄位才是對"表現層"的描述。

狀態轉化(State Transfer)

訪問一個網站,就代表了客戶端和伺服器的一個互動過程。在這個過程中,勢必涉及到資料和狀態的變化。
網際網路通訊協議 HTTP 協議,是一個無狀態協議。這意味著,所有的狀態都儲存在伺服器端。因此,如果客戶端想要操作伺服器,必須通過某種手段,讓伺服器端發生"狀態轉化"(State Transfer)。而這種轉化是建立在表現層之上的,所以就是"表現層狀態轉化"。
客戶端用到的手段,只能是HTTP協議。具體來說,就是HTTP協議裡面,四個表示操作方式的動詞:GET 、 POST 、 PUT 、 DELETE 。 它們分別對應四種基本操作: GET 用來獲取資源, POST 用來新建資源,PUT 用來更新資源,DELETE 用來刪除資源。

綜述

總結一下什麼是RESTful架構:

  • 每一個URI代表一種資源;
  • 客戶端和伺服器之間,傳遞這種資源的某種表現層;
  • 客戶端通過四個HTTP動詞,對伺服器端資源進行操作,實現"表現層狀態轉化"。

RESTful API 的設計

介紹完 RESTful 的含義,再說說 RESTful API 的設計。

協議

如果能全站 HTTPS 當然是最好的,不能的話也請儘量將登入、註冊等涉及密碼的介面使用 HTTPS。

域名

應該儘量將API部署在專用域名之下。如:

https://api.example.com

如果確定API很簡單,不會有進一步擴充套件,可以考慮放在主域名下。

https://example.org/api/

版本號

API 的版本號和客戶端 APP 的版本號是毫無關係的,不要讓 APP 將它們用於提交應用市場的版本號傳遞到伺服器,而是提供類似於v1、v2之類的 API 版本號。

版本號拼接在 URL 中。如:

api.example.com/v1/users

或是放在 Header 中:

api.example.com/users

version=v1

請求

一般來說 API 的外在形式無非就是增刪改查(當然具體的業務邏輯肯定要複雜得多),而查詢又分為詳情和列表兩種,在 RESTful 中這就相當於通用的模板。例如針對文章(Article)設計 API,那麼最基礎的 URL 就是這幾種:

  • GET /articles: 文章列表
  • GET /articles/id:文章詳情
  • POST /articles/: 建立文章
  • PUT /articles/id:修改文章
  • DELETE /articles/id:刪除文章

Token 和 Sign

API 需要設計成無狀態,所以客戶端在每次請求時都需要提供有效的 Token 和 Sign,在我看來它們的用途分別是:

  • Token 用於證明請求所屬的使用者,一般都是服務端在登入後隨機生成一段字串(UUID)和登入使用者進行繫結,再將其返回給客戶端。Token 的狀態保持一般有兩種方式實現:一種是在使用者每次操作都會延長或重置 TOKEN 的生存時間(類似於快取的機制),另一種是 Token 的生存時間固定不變,但是同時返回一個重新整理用的 Token,當 Token 過期時可以將其重新整理而不是重新登入。
  • Sign 用於證明該次請求合理,所以一般客戶端會把請求引數拼接後並加密作為 Sign 傳給服務端,這樣即使被抓包了,對方只修改引數而無法生成對應的 Sign 也會被服務端識破。當然也可以將時間戳、請求地址和 Token 也混入 Sign,這樣 Sign 也擁有了所屬人、時效性和目的地。

業務引數

在 RESTful 的標準中,PUT 和 PATCH 都可以用於修改操作,它們的區別是 PUT 需要提交整個物件,而 PATCH 只需要提交修改的資訊。但是在我看來實際應用中不需要這麼麻煩,所以我一律使用 PUT,並且只提交修改的資訊。

另一個問題是在 POST 建立物件時,究竟該用表單提交更好些還是用 JSON 提交更好些。其實兩者都可以,在我看來它們唯一的區別是 JSON 可以比較方便的表示更為複雜的結構(有巢狀物件)。另外無論使用哪種,請保持統一,不要兩者混用。

還有一個建議是最好將過濾、分頁和排序的相關資訊全權交給客戶端,包括過濾條件、頁數或是遊標、每頁的數量、排序方式、升降序等,這樣可以使 API 更加靈活。但是對於過濾條件、排序方式等,不需要支援所有方式,只需要支援目前用得上的和以後可能會用上的方式即可,並通過字串列舉解析,這樣可見性要更好些。例如:

搜尋,客戶端只提供關鍵詞,具體搜尋的欄位,和搜尋方式(字首、全文、精確)由服務端決定:

/users/?query=ScienJus

過濾,只需要對已有的情況進行支援:

/users/?gender=1

分頁:

/users/?page=2&per_page=20

響應

儘量使用 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 時就考慮少查詢一張關聯表或是少查詢 / 返回幾個欄位能帶來多大的效能提升。並且一定要以資源為單位,即使客戶端一個頁面需要展示多個資源,也不要在一個介面中全部返回,而是讓客戶端分別請求多個介面。

最好將返回資料進行加密和壓縮,尤其是壓縮在移動應用中還是比較重要的。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

You can not connect the dots looking forward, you can only connect them looking backwards.

相關文章