朱曄的互聯網架構實踐心得S2E5:淺談四種API設計風格(RPC、REST、GraphQL、服務端驅動)

Web API設計實際上是一個挺重要的設計話題,許多公司都會有公司層面的Web API設計規範,幾乎全部的項目在詳細設計階段都會進行API設計,項目開發後都會有一份API文檔供測試和聯調。本文嘗試根據本身的理解總結一下目前常見的四種API設計風格以及設計考慮點。前端

RPC

這是最多見的方式,RPC說的是本地調用遠程的方法,面向的是過程。java

  • RPC形式的API組織形態是類和方法,或者說領域和行爲。
  • 所以API的命名每每是一個動詞,好比GetUserInfo,CreateUser
  • 由於URI會很是多並且每每沒有一些約定規範,因此須要有詳細的文檔。
  • 也是由於無拘無束,HTTP方法基本只用GET和POST,設計起來比較簡單。

這裏就不貼例子了,估計超過50%的API是這種分格的。node

REST

是一種架構風格,有四個級別的成熟度:git

  • 級別 0:定義一個 URI,全部操做是對此 URI 發出的 POST 請求。
  • 級別 1:爲各個資源單首創建 URI。
  • 級別 2:使用 HTTP 方法來定義對資源執行的操做。
  • 級別 3:使用超媒體(HATEOAS)。

級別0其實就是類RPC的風格,級別3是真正的REST,大多數號稱REST的API在級別2。REST實現一些要點包括:github

  • REST形式的API組織形態是資源和實體,一切圍繞資源(級別1的要點)。設計流程包括:
    • 肯定API提供的資源
    • 肯定資源之間的關係
    • 根據資源類型和關係肯定資源URI結構
    • 肯定資源的結構體
  • 會定義一些標準方法(級別2的要點),而後把標準方法映射到實現(好比HTTP Method):
    • GET:獲取資源詳情或資源列表。對於collection類型的URI(好比**/customers**)就是獲取資源列表,對於item類型的URI(好比**/customers/1**)就是獲取一個資源。
    • POST:建立資源,請求體是新資源的內容。每每POST是用於爲集合新增資源。
    • PUT:建立或修改資源,請求體是新資源的內容。每每PUT用於單個資源的新增或修改。實現上必須冪等。
    • PATCH:部分修改資源,請求體是修改的那部份內容。PUT通常要求提交整個資源進行修改,而PATCH用於修改部份內容(好比某個屬性)。
    • DELETE:移除資源。和GET同樣,對於collection類型的URI(好比**/customers**)就是刪除全部資源,對於item類型的URI(好比**/customers/1**)就是刪除一個資源。
  • 須要考慮資源之間的導航(級別3的要點,好比使用HATEOAS,HATEOAS是Hypertext as the Engine of Application State的縮寫)。有了資源導航,客戶端甚至可能不須要參閱文檔就能夠找到更多對本身有用的資源,不過HATEOAS沒有固定的標準,好比:
{
    "content": [ {
        "price": 499.00,
        "description": "Apple tablet device",
        "name": "iPad",
        "links": [ {
            "rel": "self",
            "href": "http://localhost:8080/product/1"
        } ],
        "attributes": {
            "connector": "socket"
        }
    }, {
        "price": 49.00,
        "description": "Dock for iPhone/iPad",
        "name": "Dock",
        "links": [ {
            "rel": "self",
            "href": "http://localhost:8080/product/3"
        } ],
        "attributes": {
            "connector": "plug"
        }
    } ],
    "links": [ {
        "rel": "product.search",
        "href": "http://localhost:8080/product/search"
    } ]
}   
複製代碼

Spring框架也提供了相應的支持:spring.io/projects/sp…spring

@RestController
public class GreetingController {

    private static final String TEMPLATE = "Hello, %s!";

    @RequestMapping("/greeting")
    public HttpEntity<Greeting> greeting( @RequestParam(value = "name", required = false, defaultValue = "World") String name) {

        Greeting greeting = new Greeting(String.format(TEMPLATE, name));
        greeting.add(linkTo(methodOn(GreetingController.class).greeting(name)).withSelfRel());

        return new ResponseEntity<>(greeting, HttpStatus.OK);
    }
}
複製代碼

產生以下的結果: json

  • 除了以前提到的幾個要點,REST API的設計還有一些小點:
    • 必須無狀態的,相互獨立的,不區分順序的
    • API須要有一致的接口來解耦客戶端和服務實現,若是基於HTTP那麼務必使用HTTP的Method來操做資源,並且儘可能使用HTTP響應碼來處理錯誤
    • 須要儘可能考慮緩存、版本控制、內容協商、部分響應等實現

能夠說REST的API設計是須要設計感的,須要仔細來思考API的資源,資源之間的關係和導航,URI的定義等等。對於一套設計精良的REST API,其實客戶端只要知道可用資源清單,每每就能夠輕易根據約定俗成的規範以及導航探索出大部分API。比較諷刺的是,有不少網站給前端和客戶端的接口是REST的,爬蟲開發者能夠輕易探索到全部接口,甚至一些內部接口,畢竟猜一下REST的接口比RPC的接口容易的多。api

做爲補充,下面再列幾個有關REST API設計你們爭議討論糾結的比較多的幾個方面。緩存

建立資源使用PUT仍是POST

好比 stackoverflow.com/questions/6… ,總的來講你們基本認同微軟提到的三個方面:bash

  • 客戶端決定資源名用PUT,服務端決定資源名用POST
  • POST是把資源加入集合
  • PUT實現須要冪等

固然,有些公司的規範是建立資源僅僅是POST,不支持PUT

異常處理的HTTP響應狀態碼

  • REST的建議是應當考慮儘量使用匹配的Http狀態碼來對應到錯誤類型,好比刪除用戶的操做:
    • 用戶找不到是404
    • 刪除成功後是204
    • 用戶由於有帳戶餘額沒法刪除是409(客戶端的問題是4xx)
    • 其它服務端異常是500(服務端的問題是5xx)
  • 整體來講這個規範出發點是好的,實現起來落地比較困難,緣由有下面幾個:
    • 狀態碼對應各類錯誤類型的映射關係沒有統一標準,工程師實現的時候五花八門
    • 實現起來可能須要在業務邏輯中耦合狀態碼,很難在GlobalExceptionHandler去作,除非事先先規範出十幾種異常
    • 若是使用了不正確的響應狀態可能會致使反向代理等觸發錯誤的一些操做,並且出現問題的時候搞不清楚是哪一個層面出錯了
    • 各類Http Client對應非200狀態碼的處理方式不太一致
  • 有關這個問題的爭議,各大平臺的API實現有些聽從這個規範建議,有些是500甚至200打天下的,相關的國內外討論有:
  • 國內外的不少大廠對於這點的實現不盡相同,總的來講,個人建議是:
    • 若是咱們明確API是REST的,並且API對外使用,應當使用合適的狀態碼來反映錯誤(建議控制在20個之內經常使用的),而且在文檔中進行說明,並且出錯後須要在響應體補充細化的error信息(包含code和message)
    • 若是REST API對內使用,那麼在客戶端和服務端商量好統一標準的狀況下能夠對響應碼類型進行收斂到幾個,實現起來也方便
    • 若是API是內部使用的RPC over HTTP形式,甚至能夠退化到業務異常也使用200響應返回

返回數據是否須要包裝

看到過許多文章都在說,REST仍是建議返回的數據自己就是實體信息(或列表信息),而不建議把數據進行一層包裝(Result)。若是須要有更多的信息來補充的話,能夠放到HTTP Header中,好比https://developer.github.com/v3/projects/cards/的API:

GET /projects/columns/:column_id/cards

Status: 200 OK
Link: <https://api.github.com/resource?page=2>; rel="next",
      <https://api.github.com/resource?page=5>; rel="last"
[
  {
    "url": "https://api.github.com/projects/columns/cards/1478",
    "id": 1478,
    "node_id": "MDExOlByb2plY3RDYXJkMTQ3OA==",
    "note": "Add payload for delete Project column",
    "created_at": "2016-09-05T14:21:06Z",
    "updated_at": "2016-09-05T14:20:22Z",
    "archived": false,
    "column_url": "https://api.github.com/projects/columns/367",
    "content_url": "https://api.github.com/repos/api-playground/projects-test/issues/3",
    "project_url": "https://api.github.com/projects/120"
  }
]
複製代碼

以前咱們給出的HATEOAS的例子是在響應體中有"content"和"links"的層級,也就是響應體並非資源自己,是有包裝的,除了links,不少時候咱們會直接以統一的格式來定義API響應結構體,好比:

{
	"code" : "",
	"message" : "",
	"path" : ""
	"time" : "",
    "data" : {},
    "links": []
}
複製代碼

我我的比較喜歡這種方式,不喜歡使用HTTP頭,緣由仍是由於多變的部署和網絡環境下,若是某些環節請求頭被修改了或丟棄了會很麻煩(還有麻煩的Header Key大小寫問題),響應體通常全部的代理都不會去動。

URI的設計層級是否超過兩層

微軟的API設計指南(文末有貼地址)中指出避免太複雜的層級資源,好比**/customers/1/orders/99/products過於複雜,能夠退化爲/customers/1/orders/orders/99/products**,不URI的複雜度不該該超過collection/item/collection,Google的一些API會層級比較多,好比:

API service: spanner.googleapis.com
A collection of instances: projects/*/instances/*.
A collection of instance operations: projects/*/instances/*/operations/*.
A collection of databases: projects/*/instances/*/databases/*.
A collection of database operations: projects/*/instances/*/databases/*/operations/*.
A collection of database sessions: projects/*/instances/*/databases/*/sessions/*
複製代碼

這點我比較贊同微軟的規範,太深的層級在實現起來也不方便。

GraphQL

若是說RPC面向過程,REST面向資源,那麼GraphQL就是面向數據查詢了。「GraphQL 既是一種用於 API 的查詢語言也是一個知足你數據查詢的運行時。 GraphQL 對你的 API 中的數據提供了一套易於理解的完整描述,使得客戶端可以準確地得到它須要的數據,並且沒有任何冗餘,也讓 API 更容易地隨着時間推移而演進,還能用於構建強大的開發者工具。」

採用GraphQL,甚至不須要有任何的接口文檔,在定義了Schema以後,服務端實現Schema,客戶端能夠查看Schema,而後構建出本身須要的查詢請求來得到本身須要的數據。

好比定義以下的Schema:

#
# Schemas must have at least a query root type
#
schema {
    query: Query
}

type Query {
    characters(
        episode: Episode
    ) : [Character]

    human(
        # The id of the human you are interested in
        id : ID!
    ) : Human

    droid(
        # The non null id of the droid you are interested in
        id: ID!
    ): Droid
}

# One of the films in the Star Wars Trilogy
enum Episode {
    # Released in 1977
    NEWHOPE
    # Released in 1980.
    EMPIRE
    # Released in 1983.
    JEDI
}

# A character in the Star Wars Trilogy
interface Character {
    # The id of the character.
    id: ID!
    # The name of the character.
    name: String!
    # The friends of the character, or an empty list if they
    # have none.
    friends: [Character]
    # Which movies they appear in.
    appearsIn: [Episode]!
    # All secrets about their past.
    secretBackstory : String @deprecated(reason : "We have decided that this is not canon")
}

# A humanoid creature in the Star Wars universe.
type Human implements Character {
    # The id of the human.
    id: ID!
    # The name of the human.
    name: String!
    # The friends of the human, or an empty list if they have none.
    friends: [Character]
    # Which movies they appear in.
    appearsIn: [Episode]!
    # The home planet of the human, or null if unknown.
    homePlanet: String
    # Where are they from and how they came to be who they are.
    secretBackstory : String @deprecated(reason : "We have decided that this is not canon")
}

# A mechanical creature in the Star Wars universe.
type Droid implements Character {
    # The id of the droid.
    id: ID!
    # The name of the droid.
    name: String!
    # The friends of the droid, or an empty list if they have none.
    friends: [Character]
    # Which movies they appear in.
    appearsIn: [Episode]!
    # The primary function of the droid.
    primaryFunction: String
    # Construction date and the name of the designer.
    secretBackstory : String @deprecated(reason : "We have decided that this is not canon")
}
複製代碼

採用GraphQL Playground(github.com/prisma/grap…

其實就是__schema:
而後咱們能夠根據客戶端的UI須要本身來定義查詢請求,服務端會根據客戶端給的結構來返回數據:
再來看看Github提供的GraphQL(更多參考https://developer.github.com/v4/guides/):
查詢出了最後的三個個人repo:
GraphQL就是經過Schema來明確數據的能力,服務端提供統一的惟一的API入口,而後客戶端來告訴服務端我要的具體數據結構(基本能夠說不須要有API文檔),有點客戶端驅動服務端的意思。雖然客戶端靈活了,可是GraphQL服務端的實現比較複雜和痛苦的,GraphQL不能替代其它幾種設計風格,並非傳說中的REST 2.0。更多信息參見 github.com/chentsulin/…

服務端驅動API

沒有高大上的英文縮寫,由於這種模式或風格是我本身想出來的,那就是經過API讓服務端來驅動客戶端,在以前的一些項目中也有過實踐。說白了,就是在API的返回結果中包含驅動客戶端去怎麼作的信息,兩個層次:

  • 交互驅動:好比包含actionType和actionInfo,actionType能夠是toast、alert、redirectView、redirectWebView等,actionInfo就是toast的信息、alert的信息、redirect的URL等。由服務端來明確客戶端在請求API後的交互行爲的好處是:
    • 靈活:在緊急的時候還能夠經過redirect方式進行救急,好比遇到特殊狀況須要緊急進行邏輯修改能夠直接在不發版的狀況下切換到H5實現,甚至咱們能夠提供後臺讓產品或運營來配置交互的方式和信息
    • 統一:有的時候會遇到不一樣的客戶端,iOS、Android、前端對於交互的實現不統一的狀況,若是API結果能夠規定這部份內容能夠完全避免這個問題
  • 行爲驅動:更深一層的服務端驅動,能夠實現一套API做爲入口,讓客戶端進行調用,而後經過約定一套DSL告知客戶端應該呈現什麼,幹什麼。

以前有兩個這樣的項目採用了相似的API設計方式:

  • 貸款審覈:咱們知道貸款的信用審覈邏輯每每會變更比較大,還涉及到客戶端的一些受權(好比運營商爬蟲),並且App的發佈更新每每比較困難(蘋果App Store以及安卓各大應用商店的審覈問題)。若是採用服務端驅動的架構來告知客戶端接下去應該呈現什麼界面作什麼,那麼會有很大的靈活性。
  • 客戶端爬蟲:咱們知道若是採用服務端作爬蟲不少時候由於IP的問題會被封,因此須要找不少代理。某項目咱們想出了客戶端共享代理的概念,使用手機客戶端來作分佈式代理,由服務端驅動調度全部的客戶端,那麼這個時候客戶端須要遵從服務端的指示來作請求而後上報響應。

通常而言,對外的Web API是不會採用這種服務端驅動客戶端的方式來設計API的。對於某些特殊類型的項目,咱們能夠考慮採用這種服務端驅動的方式來設計API,讓客戶端變爲一個不含邏輯的執行者,執行的是UI和交互。

選擇哪一個模式

user-gold-cdn.xitu.io/2019/2/15/1… 此文給出了一個有關RPC、REST、GRAPHQL選擇的決策方式能夠參考,見上圖。

我以爲:

  • 在下列狀況考慮RPC風格的API或說是RPC:
    • 偏向內部的API
    • 沒有太多的時間考慮API的設計或沒有架構師
    • 提供的API很難進行資源、對象抽象
    • 對性能有高要求
  • 在下列狀況考慮REST風格:
    • 偏向外部API
    • 提供的API天生圍繞資源、對象、管理展開
    • 不能耦合客戶端實現
    • 資源的CRUD是能夠對齊的(功能完整的)
  • 在下列狀況考慮GraphQL:
    • 客戶端對於數據的需求多變
    • 數據具備圖的特色
  • 在下列狀況考慮服務端驅動:
    • 客戶端發版更新困難,須要極端的靈活性控制客戶端
    • 僅限私有API

更多須要考慮的設計點

不少API設計指南都提到了下面這些設計考量點,也須要在設計的時候進行考慮:

  • 版本控制,好比:
  • 緩存策略,好比:
    • 響應使用Cache-Control告知客戶端緩存時間(max-age)、策略(private、public)
    • 響應使用ETag來進行資源版本控制
  • 部分響應:好比大的二進制文件須要考慮實現HEAD Method來代表資源容許分段下載,以及提供資源大小信息:
HEAD https://adventure-works.com/products/10?fields=productImage HTTP/1.1

HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580
複製代碼

而後提供資源分段下載功能:

GET https://adventure-works.com/products/10?fields=productImage HTTP/1.1
Range: bytes=0-2499

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 2500
Content-Range: bytes 0-2499/4580
[...]
複製代碼
  • 列表設計:須要在設計列表類型API的時候考慮分頁、投影、排序、查詢幾點,值得注意的是列表API的額外功能比較多,儘可能進行命名的統一化規範

參考資料

相關文章
相關標籤/搜索