Web API 開發介面
目前的趨勢是向基於微服務的架構遷移,API閘道器或API門面(facade)得到了復甦。架構會分散為多個小的服務,我們需要有前置的服務(front-facing service),它們會負責為使用者暴露API。我們可以有很多微服務,因此也可以有多個針對使用者的門面,這一點絲毫不足為奇。閘道器或門面能夠讓使用者只進行一次呼叫,而不必要求使用者多次呼叫底層的微服務。這會讓API消費者的工作更加簡單,並且有助於實現更加智慧的閘道器或門面,它能夠充分利用快取(因為多次呼叫依然還有必要),應用安全功能(認證、授權)或實現特定的規則(速度限制、IP過濾)。API提供者能夠控制消費者如何使用它們的API。
服務於前端的後端
Sam Newman研究了這種消費者專用API的方式,並將其稱為“服務於前端的後端(Backends for Frontends)”BfF模式。這種模式不會為所有的客戶端建立通用的API,我們可以擁有多個BfF:一個用於Web前端、一個用於移動客戶端(甚至一個用於iOS,另一個用於Android)等。
SoundCloud採用了BfF模式,具有針對iOS平臺、Android平臺、Web站點以及嵌入式Web的API前端。與在Netflix的場景類似,如果有專門的團隊負責這些前端的話,這種技術能夠達到最佳的效果。如果你只有一個團隊負責後端及其API的話,那麼最好不要用大量不同消費者的API變種來加重他們的負擔。
微服務方面BfF對遷移很有意義:當從單體架構遷移至微服務時,某個BfF可以呼叫單體應用的功能,而其他的BfF則可以呼叫新的微服務,請遵循Strangler模式,按照這種模式,我們可以漸進式從遺留程式碼轉移到新的演進方案上。單體應用是非常複雜的,很容易積累技術債,會同時混合太多的關注因素,而微服務能夠讓我們每次只聚焦一個特定的關注點。
微服務架構也有不足。需要對其進行運維,微服務之間需要協作,它們可能會按照與大型單體應用不同的節奏進行演化。在這樣一個分散式系統中,維護所有服務之間的一致性並不簡單。眾多微服務之間的通訊可能會因為服務通訊的延遲造成延時。微服務的資料副本和反規範化(denormalization)也會帶來一定的影響,會使資料的管理和一致性複雜化。微服務並不是免費的午餐,你可以閱讀Vijay Alagarasan的文章瞭解微服務反模式的更多資訊以及Tareq Abedrabbo所撰寫的“微服務的七宗罪”。
https://www.infoq.com/articles/seven-uservices-antipatterns
https://opencredo.com/7-deadly-sins-of-microservices/
採用體驗式API或BfF的決定性因素可以歸結為它們是否有專門的團隊來維護。如果很小的組織,只有一個團隊負責後端以及面向前端或Web的API,維護眾多的變種將會更加複雜(維護成本高),但如果組織足夠大,有多個團隊能夠承擔這些前端API的任務,則可用實現。
API作為團隊溝通的模式
公司是以團隊的形式來組織的,但在越來越多的場景下,開發人員會被分為前端開發人員(Web或移動)以及後端開發人員,其中後端開發者會負責實現Web或移動裝置所需的API。Web API成為了專案交付的中心點:API是一種契約,將不同的團隊關聯了起來,能夠讓他們高效協作。
框架和工具能夠讓我們根據程式碼庫生成API定義——例如,通過註解驅動的方式,在端點、查詢引數等內容上添加註解。
但即便你自己的測試用例依然能夠通過,很小的程式碼重構也可能會破壞契約。你的程式碼庫沒有問題,但重構可能會破壞API消費者的程式碼。為了更加高效的協作,可以考慮採用API契約優先的方式,確保你的實現依然能夠遵循共享的協議:API定義。
目前,有不少可用和流行的API定義語言,如Swagger(Open API specification)、RAML或API Blueprint。你可以選擇一個最適合你的。採用API定義的方式有多項優勢。
首先,因為我們的實現需要遵循API定義,所以相容性就更不容易遭到破壞了。其次,API定義對工具化非常有利。通過API定義,我們可以生成客戶端SDK,這樣的話API的消費者就可以將其整合到他們的專案中,實現對API的呼叫,甚至可以作為skeleton,用來生成服務的初始實現。
還可以建立API的mock,這樣在底層API構建的時候,開發可以呼叫這些mock,從而避免協調API生產者和消費者之間不同的開發週期。每個團隊都可以按照自己的節奏開展工作!但是,它的好處並不侷限於程式碼和相容性,還涉及到文件。
API定義語言還會幫助我們對API實現文件化,它們會生成很漂亮的文件,展現了各種端點、查詢引數等等,並且(有時)還會提供可互動的控制檯,通過它,我們可以很容易地發起對API的呼叫。
為不同的消費者提供不同的負載
採用API契約優先的方式當然會有所幫助,並且會提供很多的收益,但是如果不同的客戶端有不同的API需求的話,該怎麼辦呢?具體來講,如果我們無法奢侈到有專門的團隊來負責不同的API門面的話,那麼該如何讓API滿足所有API消費者的需求呢?
在InfoQ最近的一篇文章中,Jean-Jacques Dubray闡述了他為什麼 停止使用MVC框架。
在這篇文章的引言中,他闡述了移動或前端開發人員如何頻繁地要求適合於他們UI需求的API,而不管底層業務理念的資料模型是什麼。Dubray所描述的狀態-行為-模型(SAM)模式能夠很好地支援BfF方式。
SAM是一個嶄新的、反應型函式式的模式,它清晰地將業務邏輯與顯示效果分開,進而簡化了前端的架構,尤其是將後端API與檢視進行了解耦。因為state和model與action和view進行了分離,所以action能夠專門服務於給定的前端或根本不進行展現:這取決於你會將游標置於什麼位置。我們還可以從中心後端或它們的門面中生成狀態表述或檢視。
Web站點或單頁應用可能需要展現產品的詳細檢視並且還要包含它的評論,但是移動裝置則很可能只展現產品的詳情和它的評分,並且允許移動使用者在點選的時候再去載入評論。根據UI的不同,流程、可用的行為、詳情等級以及查詢到的實體可能都會有所差異。
通常,在移動裝置上,我們都希望減少API呼叫獲取資料的次數,這是因為連線性和頻寬的限制,我們希望返回的負載恰好只包含所需的內容,沒有額外的資訊。但是,這一點對於Web前端來說就沒有那麼重要,通過非同步呼叫的方式,我們完全可以按照懶載入的方式載入更多的內容和資源。
不管是在哪種場景下,API顯然都需要快速響應,並具有很好的服務等級協議。但是,當我們要為不同的消費者提供多個自定義的API時,這方面有什麼可選方案嗎?
特定端點、查詢引數和欄位過濾
一種基本的方式就是提供不同的端點(/api/mobile/movie與/api/web/movie),甚至是更為簡單的查詢引數(/api/movie?format=full或/api/movie?format=mobile),不過我們可能還有更為優雅的方案。
類似於查詢引數,我們的API還可以讓使用者決定想要哪些欄位,從而自定義返回的負載,比如:/api/movie?fields=title,kind,rating或/api/movie?exclude=actors。
通過使用欄位過濾的方式,我們還可以確定是否要在響應中包含相關的資源:/api/movie?includes=actors.name。
自定義MIME媒體型別
作為API的實現者,我們還有其他的可選方案。我們可以根本不提供任何的自定義的功能!消費者要麼接受我們提供的API,要麼將API包裝到他們自己的門面中,在這個門面中,他們可以構建想要的自定義功能。
因為我們都是非常友善的人,所以會給他們提供多個profile:在媒體型別方面,我們可以發揮創造性,根據消費者所請求的媒體型別不同,返回更加精簡或更加豐富的負載。例如,如果你看一下GitHub API (https://developer.github.com/v3/media/)的話,可能會注意到這樣的型別:application/vnd.github.v3.full+json
通過使用“full” profile,API會提供完整的負載和相關的實體,你還可以使用“mobile”或“minimal”變種的profile。
API消費者在發起呼叫時,就可以請求最適合其使用場景的媒體型別。
Prefer頭資訊
Irakli Nadareishvili曾經寫過一篇文章,介紹了API中客戶端優化的資源表述,
http://www.freshblurbs.com/blog/2015/06/25/api-representations-prefer.html
提到了一個鮮為人知的頭資訊域:Prefer頭資訊(RFC 7240)。
與自定義媒體型別類似,客戶端可以使用Prefer頭資訊請求特定的profile:使用Prefer: return=mobile能夠讓API響應自定義的負載並且會帶上Preference-Applied: return=mobile的頭資訊。注意的是,當使用Prefer頭資訊的時候,需要對應地使用Vary頭資訊。
作為API開發人員,如果我們要負責確定支援哪種型別的負載,那麼你可能會喜歡自定義媒體型別、Prefer頭資訊或專門的端點。如果你想要客戶端能夠更加靈活地確定要檢索哪些欄位或關聯關係的話,那麼你可能會更中意欄位過濾或查詢引數的方案。
GraphQL
與其React檢視框架一起,Facebook為開發人員引入了GraphQL。在這裡,消費者能夠完全控制會接受到什麼樣的負載結果,包括欄位及關聯關係。消費者在發起請求時,指定了返回的負載應該是什麼樣子的:
{
user(id: 3500401) {
id,
name,
isViewerFriend,
profilePicture(size: 50) {
uri,
width,
height
}
}
}
API所響應的負載應該會像如下所示:
{
"user" : {
"id": 3500401,
"name": "Jing Chen",
"isViewerFriend": true,
"profilePicture": {
"uri": "http://someurl.cdn/pic.jpg",
"width": 50,
"height": 50
}
}
}
GraphQL會作為一個查詢,同時還會作為迴應該請求的描述。GraphQL讓API的消費者能夠完全控制返回的內容,提供了最高級別的靈活性。
在規範方面,還有一種類似的方式,那就是OData,它能夠讓我們通過$select、$expand和$value引數來自定義負載。但是OData有點落伍,處在被拋棄的邊緣,前段時間Netflix和eBay已經宣佈不再支援OData,而其他的參與者,如微軟和SalesForce對它依然提供支援。
超媒體API
最後一個要討論的可選方案就是超媒體API。當提到超媒體API的時候,你通常會想到那些額外的超連結會讓響應變得凌亂,它們可能會讓負載的大小成倍增加。對於移動裝置來說,負載的大小和呼叫的次數確實值得關注。儘管如此,非常重要的一點在於,我們要通過HATEOAS(超媒體作為應用狀態引擎)來思考超媒體,它是一個經常被忽視的REAT API核心原則。它與API所提供的功能有關。消費者可以訪問相關的資源,但是這些超媒體關係所提供的連結也可以作為不同的可選profile,比如:
{ "_links": { "self": { "href": "/movie/123" }, "mobile": { "href": "/m/movie/123" }, } }
另外,有些超媒體方式能夠完全接受嵌入式關聯實體的理念。Hydra、HAL和SIREN提供了嵌入子實體的功能,所以我們在獲取一部電影的資訊的時候,也能以嵌入式列表的形式列出它的所有演員。
在一篇關於如何選擇超媒體格式的文章中,Kevin Sookocheff給出了一個樣例,展示瞭如何訪問“玩家的朋友列表”,在這個資源訪問中,嵌入了這些好友的實際表述而不僅僅是這些資源的連結,因此能夠減少對每個好友資源的訪問:
{ "_links": { "self": { "href": "https://api.example.com/player/1234567890/friends" }, "size": "2", "_embedded": { "player": [ { "_links": { "self": { "href": "https://api.example.com/player/1895638109" }, "friends": { "href": "https://api.example.com/player/1895638109/friends" } }, "playerId": "1895638109", "name": "Sheldon Dong" }, { "_links": { "self": { "href": "https://api.example.com/player/8371023509" }, "friends": { "href": "https://api.example.com/player/8371023509/friends" } }, "playerId": "8371023509", "name": "Martin Liu" } ] } }
小結
Web API所面臨的消費者種類在持續增加,有著不同的需求。微服務架構會鼓勵我們針對這些需求部署細粒度的API門面(也就是所謂了體驗式API或BfF模式),但是如果你要滿足太多不同消費者的需求的話,這可能會成為一種反模式,如果你只有一個很小的團隊來應對所有前端的話,那麼這種問題會更加嚴重。
一定要進行必要的權衡!在準備採用某種方式之前,你需要學習可選方案的成本並考慮你是否能夠支援這些方案。建立API的不同變種是有成本的,對於實現API的人和消費API的人來說均是如此,這取決於所採用的策略。此外,在釋出API給消費者之後,你可能需要重新思考和重構這個API,因為在設計階段,你可能沒有充分考慮這些特定的裝置或客戶需求。
如果你有專門的團隊來負責這些API門面,那麼這是一種可行的方案。如果你沒有這麼奢侈的團隊的話,有一些其他的方式來為消費者提供自定義負載,而且能夠避免引入複雜性,這包括一些簡單的技巧,如欄位過濾或Prefer頭資訊,也包括全面的解決方案,如自定義媒體型別或GraphQL這樣的規範。
但是,我們也不一定非得這樣大動干戈,可以採用一種中間方案:一個主要的、完整的API,再加上一個或兩個針對移動裝置的變種,這樣的話,很可能就已經滿足了所有消費者的需求。再考慮增加一些欄位過濾功能,這樣每個人都會對你的API表示滿意!