一篇來自前端同窗對後端接口的吐槽

前言

去年的某個時候就想寫一篇關於接口的吐槽,當時後端提出了接口方案對於我來講調用起來很是難受,但又說不上爲何,沒有論點論據因此也就做罷。最近由於寫全棧的緣故,團隊內部也遇到了一些關於接口設計的問題,因而開始思考實現接口的最佳實踐是什麼。在參考了許多資料以後,逐漸對這個問題有了本身的理解。同時回想起過去的經驗,終於恍然大悟本身當時的痛點在哪裏。javascript

既然是吐槽,那麼請原諒我接下來態度的不友善。本文中列舉的全部例子都是我我的的親身經歷。前端

誰應該主導接口的設計

或者更直白一些,應該是接口的消費方仍是提供方來決定接口的設計?java

固然是接口的消費方git

「接口」最弔詭的地方在於提供方大費周章把它實現了,但它本身卻(幾乎)重來都不使用。因而這極易陷入一種自嗨的境地,由於他更本不知道接口的好壞。就比如一個歷來不嘗本身作的菜的廚子,你期望他的菜能好到哪裏去,他的廚藝能好到哪裏去。上面隱含的前提是(我認爲)接口是有絕對好壞之分的,壞的接口消費者調用難受,提供者維護難受,還致使產品行爲彆扭體驗變差。github

然而接口的好壞與誰來主導設計有什麼關係?由於壞接口產生的緣由之一是提供方只站在開發者的角度解決問題:web


例子一 ( Chatty API )

某次須要實現容許用戶建立儀表盤頁面的功能(若是你對儀表盤頁面感到陌生的話,能夠想象它是一張集中了不一樣圖表的頁面,好比柱狀圖、折線圖、餅圖等等。用戶能夠添加本身想要的圖表到頁面中,而且手動調整它們的尺寸和位置。儀表盤一般用於總覽某個產品或者服務的運行狀態)。後端同窗的接口初步設計是,當用戶填寫完基本信息、添加完圖表、點擊建立按鈕以後,我須要連續調用兩次接口才能完成一次儀表盤的建立:數據庫

  1. 利用用戶填寫的基本信息以及圖表的尺寸和位置建立一個空的儀表盤
  2. 再向儀表盤中填充圖表的具體信息,好比圖表類型,使用的維度和指標等

很明顯看出他徹底是按照本身後端的存儲結構在設計接口,不只是存儲結構,甚至存儲過程都盡收眼底。想象一種極端的狀況,那不僅提供一些更新數據庫表的接口得了,前端本身把經過接口把數據插入庫中redux

面對這類底層性質的接口,消費者在集成時須要考慮接口的調用步驟以及理解背後的原理。若是後端的底層結構一旦發生更改,接口頗有可能也須要發生更改,前端的調用代碼也須要隨之更改。後端

後端研發可能會辯解說:後端用了微服務啊,不一樣類型的數據存儲在不一樣的服務上,因此你須要和不一樣的服務通訊才能實現完整的存儲。他們始終沒有明白的事情是,後端的實現致使了接口的碎片化,那是你的問題,而不該該把這部分負擔轉移到前端的開發者上,其實也是間接轉移到了用戶身上。不要欺負我不懂後端,至少我瞭解加一層相似於 BFF 的 Orchestration Layer 就能解決這個問題api

Netflix 的工程師 Daniel Jacobson 在他的文章 The future of API design: The orchestration layer 中指出, API 無非是要面對兩類受衆:

  1. LSUD: Large set of unknown developers
  2. SSKD: Small set of known developers

隨着產品服務化的趨勢,頗有可能須要像 AWS 或者 Github 那樣對公共開發者即 LSUD 暴露接口。且不說上面例子中的接口方案會不會被唾沫星子淹死,如此明顯的暴露內部服務的細節是很是危險的事情。

因此在設計接口時,應該讓消費者來主導。若是消費者沒能給出很好的建議,那麼至少提供者在設計時也應該站在消費者的立場上來思考問題。又或者,至少想想若是你本身會樂意使用用你本身設計出來的接口嗎?

使用後端思惟設計接口不只體如今 URI 的設計上,還有可能體如今請求參數和返回體結構上:


例子二

假設如今須要一個請求批量文章的接口,接口同時返回多篇文章的內容,包括這些文章的內容,做者信息,評論信息等等。

理想狀況下,咱們指望返回的數據是以文章爲單位的,好比

articles: [
	{
  		id: ,
        author: {},
        comments: []
	},
    {
    	id:
        author: {},
        comments: []
    }
]
複製代碼

However, 後端的返回結果多是以實體爲單位:

{
    articles: [],
    authors: [],
    comments: []
}
複製代碼

comments 裏包含不一樣文章的 comment,我必須經過相似於 articleId 的字段對它們執行 group by 操做才能分離出屬於不一樣文章的評論。對其餘實體作一樣的操做,最終手動的拼接成前端代碼須要的 articles 數據結構

很明顯這又是按照後端庫表關係返回的結果,嚴格來講這並不算是 anti-pattern,在 redux 中也鼓勵將數據 normalize。但若是前端用不到原始數據,請不要返回原始數據。例如我須要在頁面上展現一個百分比格式的數據,除非用戶有動態調整數據格式的需求,例如千分位、小數或者是切換精度等等,不然就直接返回給我百分比的字符串就行了,不要返回給我原始的浮點數據。前端對數據的二次加工還會給問題排查帶來干擾,若是任何數據都須要前端進行二次加工,那麼因此問題的排查都必須從前端發起,前端確認無誤後再進入後端排查流程,這始終會佔用兩個端的人力,而且 delay 了排查的進度

關於 meta 信息


例子三:

後端接口在返回時一般會帶上 meta 信息,meta 信息包含接口的狀態以及若是失敗時的失敗緣由,便於調試使用。後端提供的接口的 meta 信息的數據結構以下:

{
    meta: {
      code: 0,
      error: null,
      host: "127.0.0.1"
    },
   	result: []
}
複製代碼

在我看來,以上數據結構有兩個問題

meta 信息包含獨立的狀態信息

在包含狀態碼的 meta 信息接口設計中,一條默認的隱藏邏輯是:接口返回的 HTTP status code 必定是 200,數據是否真的獲取成功須要經過 meta 裏的自定狀態碼 code 進行判斷(換句話說,上面你看到的接口其實是「接口的接口」)。最終在前端的代碼中也不須要經過 HTTP code 判斷返回是否正常,只須要判斷接口裏返回的meta.code便可

**可是誰給大家的自信保證後端接口必定是不會掛的?!**不管後端如何保證接口的堅固,前端仍然須要首先判斷 HTTP code 是否爲 200,再判斷meta.code是否與預期的符合一致。這和信任無關,和我程序的健壯有關。

既然不管如何都要對接口判斷兩次,那爲何不將meta.code與 HTTP code 合二爲一?更況且我還須要再本地維護一份自定義 code 的枚舉值,還須要和後端保證同步。這就涉及到下一個問題了:

meta 信息的存放位置

咱們須要 meta 信息沒有錯,可是咱們沒有那麼須要 meta 信息。這體如今幾點:

  1. 咱們真的須要一個平行於返回結果的字段展現 meta 信息嗎?
  2. 每一次請求咱們都須要 meta 信息嗎?
  3. meta 信息必定要在 meta 字段裏嗎?

以請求失敗的錯誤信息爲例,錯誤信息只會出如今接口非正常返回的狀況下,但咱們應該始終在返回體中用一個字段爲它預留位置嗎?

在關於 meta 信息存在位置的這個問題上,我傾向於將它們整合進入 HTTP Header 中。例如meta.code徹底可使用 HTTP code 代替,我看不出始終要保證 200 返回以及自定義 code 的意義在哪裏

而至於其它的 meta 信息,能夠經過以X-開頭的自定義 HTTP Header 進行傳遞。例如

Github API 中關於使用頻率限制的信息就放在 HTTP Header 中:

Status: 200 OK
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1372700873
複製代碼

Design for today


例子四

咱們須要爲某個指標的折線圖設計查詢接口,查詢以天爲單位,也就是說該接口只會根據查詢的日期返回指定日期的查詢結果,後端提供的返回數據結構以下:

{
    data: [
        {
            date: "2019-06-08",
            result: [
                
            ]
        }
    ]
}
複製代碼

雖然需求很明確的指示只會返回某天的查詢結果,可是後端仍是決定給我返回一個數組。他這麼設計的理由是爲了防止往後需求發生改變須要返回多日的查詢結果。

這看上去是很聰明決策:「看,我預見性的 cover 了一個將來的需求!」,但實際上愚蠢至極:你的確 cover 了一個需求,不過是一個當前並不存在,將來也不見得會發生的需求;並且若是你真的想寫 future-proof 的代碼,那麼還有將來千千萬萬的需求等待着你實現。

問題在於沒有人知道未來是否真的會容許同時查詢多日數據,即便某天須要支持同時查詢多日數據了,數據結構也不必定非要如此。在數據分析領域咱們面臨的查詢需求並非線性從單個到多個,在其餘業務領域也是這樣。

這樣致使的後果是你花費多餘的時間實現了不須要的代碼,而且前端也須要配合這樣的數據結構進行實現。而且在未來的維護中,每一個看到返回體是數組的人都會納悶爲何返回的結果明明只有一條,還須要用數組封裝,是否是我遺漏了什麼?因而不得不投入精力來驗證是否真的有可能返回更多的數據。API 和代碼應該是精準的,準確表達你想實現的一切而不存在有歧義

有人可能會說不就是多了一層封裝嗎?實現上也花不了多少的功夫何至於大驚小怪。抱歉我不是針對這一個 case,而是在強調任何場景下不管實現的難易都不該該添加無心義的代碼,「勿以惡小而爲之」就是這個道理

「關注當下」還有另外一個維度含義:


例子五

目前咱們已經有建立單個文章的接口,如今須要支持批量建立文章。後端給出的建議是:不如調單個接口屢次?

例子六

目前已經有一個接口可以取得文章相關數據,好比內容、評論、做者等等。如今咱們須要增長一個新的頁面用於展現用戶信息。後端給出的建議是:不如使用文章數據接口,裏面已經包含了做者信息,這樣就不用開發新的接口了


以上的例子看似都是想實現對接口的複用,但實際上起到的是事倍功半的效果

在例五中,雖然語義上「建立五篇文章」和「連續五次建立一篇文章」是等效的,可是在實現和操做層面並非如此。且不說調用五次和調用一次的性能大不相同,批量建立的五篇文章可能存在順序關係,可能須要事務操做。

在例六中雖然可以達到咱們實現的效果,但這不能算是接口的複用,只能算是接口的 hack(hack 和複用的區別在因而否用物品的初衷功能作事情)。而且 hack 接口是有風險的,對於接口的提供者而言他們更關心接口服務「正統」的消費者,在這個 case 中接口的存在是爲了展現完整的文章信息,若是有一天「文章信息」這個需求發生了變化頗有可能會致使做者信息同時發生變化,縮減字段甚至取消字段。那麼它們沒有義務這些 hack 用戶負責。一個接口本應該就專一一件事情

因此最理想的事情是,爲當前專一的業務開發獨立的接口。在例六的例子中,可能咱們在開發一個獨立請求做者的信息的接口時實現代碼徹底複製自另外一個接口的實現,可是接口的隔離在長遠看來能給功能的維護帶來更大的便利

不只限於 REST API

「接口」是一個概念。在概念之下如何實現它咱們擁有不少種選擇。目前看來絕大部分的方式是經過 REST API 來達成的,也並無什麼事情是 REST API 沒法作到的,但事實上這幾年技術的進步給了咱們更多的選擇,若是選擇更有針對性的實現方案,效果會更好

例如在實時數據的場景下,理論上是由後端(有數據更新時)驅動前端視圖的更新,這理應是 push 操做。可是在傳統實現中,咱們不得不仍然經過被動的等待和輪詢實現功能。

對於事件驅動類型的需求使用 WebSocket 或者是 Streaming 彷佛是更好的選擇。若是是後端之間的交互還能夠利用 WebHook。我一般對新技術持保留態度,可是不得不認可 GraphQL 在處理某些需求上也可以比 REST API 作的更好。而且大部分廠商對於 GraphQL 接口的支持代表它是可行的。

我瞭解實現 API 來只是後端實現功能的一個很小的環節,在接口背後是更多業務邏輯的修改和庫表結構的更迭。甚至說接口部分有一半都是交給框架來實現的。可是,哪怕只有很小的機會,也應該把這個環節作到盡善盡美。

結束語

對於糟糕的接口設計我還能繼續沒完沒了的抱怨下去,但忽然然以爲洋洋灑灑的繼續寫下去彷佛沒有太大意義。講真我不是來真的大吐苦水的,只是想表達接口設計也相當重要。在工做中痛心的看到不少問題明明用一些很基礎的技巧就可以解決,而你們卻對它熟視無睹以形成兩敗俱傷的境地。以上就是我認爲的在接口設計中須要遵循的一些原則和考慮要素,相信可以解決大多數的痛點和避免部分的問題

後端同窗們,若是大家有心讓接口變得更好,多聽聽「消費者」的反饋。若是大家嘗試使用過第三方接口開發過應用的話,例如 Slack、Github,你會發現它們的接口是在不斷迭代的。不斷有舊的接口被淘汰,新的接口投入使用。這種迭代背後不是閒着沒事幹,而是出於實際的用戶的聲音和需求

最後推薦我最近閱讀的關於 API 設計的圖書,收益匪淺:

  • Web API 的設計與開發
  • Designing Web APIs
  • APIs A Strategy Guide

本文同時發佈在個人知乎專欄,歡迎你們關注

相關文章
相關標籤/搜索