怎樣編寫好的API?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"本文最初發表於"},{"type":"link","attrs":{"href":"https:\/\/www.stxnext.com\/blog\/how-to-build-a-good-api-that-wont-embarrass-you","title":"","type":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"STX Next博客網站"}]},{"type":"text","marks":[{"type":"strong"}],"text":",經原作者Sebastian Buczyński同意由InfoQ中文站翻譯分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,每個人都在關注API。API最早開始流行於大約20年前,2000年,Roy Fielding在他的博士論文中首次提出了REST這個術語。同年,Amazon、Salesforce和eBay向全世界的開發者介紹了他們的API,永遠改變了我們構建軟件的方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在REST之前,Roy Fielding論文中的原則被稱爲“HTTP對象模型”,隨後你會明白這爲何非常重要。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隨着閱讀的深入,你還會看到如何確定你的API是否成熟,好API的主要品質是什麼以及爲何在構建API的時候,要注重適應性。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"RESTful架構基礎"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST代表表述性狀態轉移(Representational State Transfer),由Roy Fielding在他的博士論文中定義,長期以來,它就是服務API的聖盃。它並不是構建API的唯一方式,但是由於它的流行,即便是非開發人員也知道這種標準。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RESTful軟件有如下六種特點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"客戶端-服務器端架構"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"無狀態"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"可緩存"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"分層系統"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"按需編碼(可選)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"統一接口"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,對日常使用來說,這過於理論化了。我們需要更具操作性的東西,這也就是API成熟度模型。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Richardson成熟度模型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該模型是由Leonard Richardson提出的,它將RESTful開發原則結合成四個簡單易行的步驟。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/75\/753f4add733015c0fb3752a01bc2002a.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在模型中的位置越高,就越接近Roy Fielding所定義的RESTful原始理念。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Level 0:POX(Plain Old XML)的泥沼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Level 0的API是一組簡單XML或JSON的描述。在前文中,我曾經說過在Fielding的論文之前,RESTful原則被稱爲“HTTP對象模型”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是因爲HTTP是RESTful開發中最重要的組成部分。REST要儘可能多地使用HTTP固有屬性中的理念。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Level 0,沒有使用任何這樣的東西。我們只是構建自己的協議並把它作爲一個專有層。這種架構被稱爲遠程過程調用(Remote Procedure Call,RPC),適用於遠程過程\/命令。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常我們會有一個端點,可以對它進行調用以獲取一堆XML。在這方面,一個典型的例子就是SOAP協議:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/97\/976134016402f7d1654992feb0d16e60.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外一個很好的例子就是Slack API。它有些多樣化,有多個端點,但依然是RPC風格的API。它暴露了Slack的各種功能,中間沒有附加任何特性。如下的代碼展示瞭如何向一個特定的通道發送消息:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/51\/517deffff1b1e1b957ca6d32690c9e87.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然按照Richardson的模型,這是一個Level 0的API,但是這並不意味着它是不好的。只要它是可用的,並且恰當地服務於業務需求,那它就是很棒的API。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Level 1:資源"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了構建Level 1的API,我們需要找出系統中的名詞並將它們通過不同的URL暴露出來,如下面的樣例所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/68\/686b5ce2484955677e65b7bc67d6dae3.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中,“\/api\/books”能讓我訪問一個通用的圖書目錄,“\/api\/profile”能夠讓我訪問這些書的作者的基本信息。爲了獲取某個資源的第一個特定實例,我可以在URL中添加ID(或其他引用)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在URL中還可以嵌套資源,這展示了它們是以層級結構的形式組織的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回到Slack的樣例,如下展示了按照Level 1 API,它們會是什麼樣子的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/5f\/5fa57bc77520b436c402e2139379c57b.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,URL發生了變化,從原先的“\/api\/chat.postMessage”變成了現在的“\/api\/channels\/general\/messages”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"信息中“channel”部分從請求體轉移到了URL中。從字面就能看出,通過使用這個URL,我們可以預期有條消息發佈到了“"},{"type":"text","marks":[{"type":"italic"}],"text":"general"},{"type":"text","text":"”通道上。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Level 2:HTTP動作"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Level 2利用HTTP動作(verb)來添加更多的含義和意圖。在這方面可用的動作比較多,我這裏只用到一個基礎的子集:PUT \/ DELETE \/ GET \/ POST。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"藉助這些動作,我們可以預期包含它們的URL有不同的行爲:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"POST:創建新數據"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PUT:更新現有的數據"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"DELETE:移除數據"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GET:查找特定id的數據輸出,獲取某個資源(或整個集合)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上面提到的“\/api\/books”爲例:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ec\/ec06b380deed723197283a85e16535f0.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那這裏的“安全”和“冪等”又是什麼意思呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“安全”的方法指的是永遠不會改變數據的方法。REST建議GET方法只能用來獲取數據,所以在上面的集合中,它是唯一一個安全的方法。不管你調用多少次基於REST的GET方法,它永遠不會改變數據庫中的任何東西。但是,這並不是該動作的固有特性,而是關係到你該如何實現它,所以我們需要確保它是這樣運行的。所有其他的方法都會以不同的方式改變數據,不能隨意使用。在REST中,GET方法既是安全的,又是冪等的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“冪等”的方法指的是多次使用不會產生不同結果的方法。按照REST,DELETE方法應該是冪等的,如果刪除了某個資源,然後針對相同的資源再次調用DELETE,它不會改變任何東西。資源應該早就已經消失了。在REST規範中,POST是唯一一個非冪等的方法,所以我們可以對相同的資源多次調用POST方法,這樣我們會得到重複的資源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們重新看一下Slack樣例,如果我們使用HTTP動作來進行更多的操作會是什麼樣子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/dc\/dc65f5123e4d75a5ca693c3250e1e00d.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以使用POST方法發送消息到通用的通道,我們也可以使用GET方法從通用通道獲取消息。我們還可以使用DELETE方法和特定的ID刪除消息,這裏比較有意思的一點在於,消息並不是與特定通道關聯的,所以我可以設計一個單獨的API來刪除資源。這個例子表明,設計API並不總是那麼簡單,這方面有很多可選項和權衡。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Level 3:HATEOAS"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還記得純文字、沒有任何圖像的電腦遊戲嗎?我們只能看到一些文本,描述了你在哪裏,以及接下來能幹什麼。爲了取得進展,我們必須要輸入自己的選擇。在一定程度上來講,HATEOAS就是做這件事情的。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/84\/84ae732d51f4136e5ebc8112e81ccd3d.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HATEOAS指的是“超媒體作爲應用狀態引擎(Hypermedia as the Engine of Application State)”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了HATEOAS之後,當其他人使用你的API的時候,他們就能看到通過API還能做哪些其他的事情。HATEOAS回答了“從這裏出發,我還能去哪裏?”的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/32\/324ea9a1cdc56cbf5ceb14560c68bf22.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但這還不是所有的內容。HATEOAS還可以對數據關係進行建模。我們可能會有一個關於圖書的資源,並且在URL中沒有將作者信息嵌套進來,但是我們可以包含它們的鏈接,如果有人對作者感興趣的話,那麼他們可以訪問這些鏈接並探索相關的數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HATEOAS不像其他成熟度模型的等級那樣流行,但是有些開發人員確實在使用它。其中一個樣例就是Jira,如下是它們的搜索API的響應:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a3\/a3ba14e0a106e4cb3c5885c78490e600.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"他們將鏈接嵌入到了其他我們可以探索的資源中,以及該issue的狀態過渡列表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外一個使用HATEOAS的樣例是Artsy。他們的API嚴重依賴HATEOAS,並且還使用了JSON Plus調用規範,按照該規範強制要求使用一種特殊的約定來構建鏈接。下面是一個分頁的例子,這是使用HATEOAS最酷的樣例之一:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a0\/a0c07b91635d474de3150f423fdfc3de.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以提供到下一頁、上一頁、第一頁和最後一頁的鏈接,還可以按照需要添加其他頁面的鏈接。這樣簡化了API的消費,因爲這樣不需要在客戶端添加URL的解析邏輯,也不需要追加頁碼的方法。我們只需要在客戶端使用已經實現結構化的鏈接就可以了。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"好的API由什麼組成"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們已經介紹完了Richardson模型,但這並不是實現好的API的全部內容。其他重要的品質還有什麼呢?"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"錯誤\/異常處理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我對自己使用的API的基本期望之一就是,需要有一種明確的方式來判斷是否有錯誤或異常。我想要知道請求是否得到了處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HTTP有一種簡單的方式來實現這一點:HTTP狀態碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"管理狀態碼的基本規則是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2xx代表一切正常"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3xx代表你想要找的公主在另外一個城堡,也就是你要找的資源在其他的地方"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4xx代表客戶端做錯了某些事情"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5xx代表服務器端失敗"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1d\/1de1292432698877e45fc756952f818e.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們的API至少要提供4xx和5xx狀態碼。有時候,5xx是自動生成的。例如,客戶端發送了一些內容到服務器端,但是這非法的請求,而我們的校驗是有缺陷的,從而導致這個問題繼續在代碼中執行了下去,最終導致出現了異常,這樣就會返回一個5xx的狀態碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你想要承諾使用特定的狀態碼,那麼你會遇到“哪種狀態碼最適合當前情況?”的問題。這樣的問題並不總是那麼容易回答,我推薦你去閱讀聲明這些狀態碼的RFC,它們給出了比其他來源更廣泛的解釋,並且告訴了你何時使用這些狀態碼更合適等。幸運的是,網上有些資源可以幫助我們做出選擇,比如"},{"type":"link","attrs":{"href":"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Status","title":"","type":null},"content":[{"type":"text","text":"Mozilla的HTTP狀態碼指南"}]},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"文檔"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優秀的API必須要有優秀的文檔。在文檔方面,最大的問題在於,隨着API的發展需要找人同步更新文檔。有個更好的方案是不脫離代碼自更新文檔。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,註釋與代碼的脫節。當代碼發生變化的時候,註釋依然保持不變,這樣的話,註釋就過時了。這甚至會比根本就沒有任何註釋更糟糕,因爲在隨後的一段時間內,它們會提供錯誤的信息。註釋不會自動更新,所以開發人員需要記得在維護代碼的時候同時維護它們。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自更新的文檔工具可以解決這個問題。在這方面,一個流行的工具就是Swagger,它是基於OpenAPI構建的工具,可以很容易地描述你的API。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a0\/a0c886829cda281fe0e67fc847ded427.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Swagger很酷的一點在於它是可執行的,所以如果你嘗試修改API,能立即看到它的作用和變化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了給Swagger添加自動更新功能,我們需要使用其他的插件和工具。在Python中,有針對大多數主流框架的插件。它們能生成API請求該如何組織的描述,並定義數據的輸入和輸出。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你不想要使用Swagger,而是想使用更簡單的工具,那該怎麼辦呢?有個流行的替代方案是"},{"type":"link","attrs":{"href":"https:\/\/slatedocs.github.io\/slate\/#introduction","title":"","type":null},"content":[{"type":"text","text":"Slate"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/04\/04cf2a7531c70aa7f2e1b57e6f94926f.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有一些值得推薦的中間方案,如"},{"type":"link","attrs":{"href":"https:\/\/github.com\/Mermade\/widdershins","title":"","type":null},"content":[{"type":"text","text":"widdershins"}]},{"type":"text","text":"和"},{"type":"link","attrs":{"href":"https:\/\/api2html.com\/docs\/overview\/","title":"","type":null},"content":[{"type":"text","text":"api2html"}]},{"type":"text","text":"的組合,它允許我們從Swagger的定義中生成類似Slate的文檔。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"緩存"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在有些系統中,緩存可能並不是什麼大問題。這樣的系統可能沒有很多的數據可供緩存,所有的數據都在不斷地發生變化,或者系統根本沒有很大的流量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,在大多數情況下,緩存對於良好的性能至關重要。它與RESTful API密切相關,因爲HTTP協議在緩存方面做了很多事情,比如HTTP頭信息允許我們控制緩存的行爲。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能想要在客戶端緩存東西,或者如果有註冊表或值存儲的話,那麼你可能想要在應用程序中緩存數據。但是,HTTP讓我們能夠基本上免費就可以獲得一個很好的緩存,所以如果可能的話,請不要錯過這個免費的午餐。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ea\/ea51ea91c288ba5aa0a4d3c001071f2e.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時,因爲緩存是HTTP規範的一部分,所以很多涉及HTTP的技術都知道如何進行緩存:瀏覽器原生支持緩存,客戶端和服務器之間的中間技術也是如此。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"API設計的演化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"構建API以及現代軟件最重要的部分就是適應性。如果沒有適應性,開發就會變慢,在合理的時間發佈特性就會變得更加困難,當面對最後截止時間的時候更是如此。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“軟件架構”在不同的上下文語境中有不同的含義,不過我們現在採用這個定義:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"軟件架構一種行爲\/藝術,能夠避免會阻礙未來變化的決策。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"記住了這一點,在設計軟件的時候,當你必須要在具有相似優點的方案中做出選擇時,你應該始終選擇更多考慮到未來的方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好的實踐並不是萬能的。按照正確的方式構建錯誤的東西並不是你想要的結果。最好採取一種成長的心態,接受變化是不可避免的,尤其是如果你的項目要持續發展的話更是如此。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要想讓你的API更具適應性,其中很關鍵的一點就是保持儘可能薄的API層,真正的複雜性應該往下層轉移。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"API不應該限定實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"公開的API發佈之後,它就已經完成了,是不可改變的,你就不能再去觸碰它了。如果你已經有了一個設計古怪的API,除了接受現狀之外,還能做些什麼呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你應該不斷尋找簡化實現的方法。有時候,你可以通過一個特定的HTTP頭信息來控制API響應的格式,相對於構建另外一個叫做v2的新API,這是一種更簡單的解決方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"API只是另外一層的抽象。它們不應該決定如何實現,爲了避免這種問題,我們可以採用如下幾種開發模式。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"API網關"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是一種類似於門面的開發模式。如果你要把一個單體結構拆分爲一組微服務,並且希望向外部暴露一些功能的話,那麼你只需要構建一個類似門面的API網關。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它將爲不同的微服務提供一個統一的接口(這些微服務可能有不同的API,使用不同的錯誤格式等等)。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"適用於前端的後端"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你必須要構建一個API來滿足一堆不同的客戶端的話,那麼這可能會非常困難。針對某個客戶端所作出的決策可能會影響其他客戶端的功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照適用於前端的後端(backend for frontend)理念,如果你有不同的客戶端,它們喜歡不同形式的API,比如移動應用可能會喜歡使用GraphQL,那麼就單獨爲它們構建吧。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只有當你的API是一層抽象,並且這個抽象層很薄的時候,這種方式纔有效。如果它與你的數據庫耦合,或者太大,具有太多的邏輯,那麼就無法這樣做了。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"GraphQL與RESTful"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很多人都在熱炒GraphQL。它是一項新興的技術,但是已經有了很多粉絲,以至於有些開發者聲稱它將取代REST。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘管GraphQL比RESTful要新的多,但是它們有很多相似之處。GraphQL最大的不足之處在於它的緩存,它必須要在客戶端或應用程序中實現。現在,有內置的實現了緩存功能的客戶端庫(比如"},{"type":"link","attrs":{"href":"https:\/\/www.apollographql.com\/docs\/react\/caching\/cache-configuration\/","title":"","type":null},"content":[{"type":"text","text":"Apollo"}]},{"type":"text","text":"),但是這仍然要比使用HTTP提供的幾乎免費的緩存功能要困難。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從技術講,GraphQL位於Richardson模型的Level 0層級,但是它具有良好API的特質。我們可能無法同時使用多個HTTP的功能,但是GraphQL的出現就是解決這一問題的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GraphQL的殺手鐗就是聚合不同的API,並將它們作爲一個GraphQL API暴露出來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/b7\/b7c49c32c9a2c71be62d1111ac5b7973.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GraphQL在處理數據抓取不足和數據過量抓取方面有很好的效果,而這些問題是REST API很難進行管理的。這兩個問題都與性能有關,如果數據抓取不足,那說明你沒有高效地使用API,所以必須要進行大量的調用。如果數據過量抓取的話,那麼API調用的數據傳輸會比必要的數據傳輸更大,這是對帶寬的一種浪費。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"小結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"藉助REST與GraphQL的比較,我們能夠總結出一個好的API最重要的品質。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好的API的特性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/34\/34ed63f23989861a74525adfaeb63a60.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們需要一個清晰的數據表述方式:RESTful以資源的方式提供了表述。我們需要有一種方式顯示有哪些可用的操作:RESTful通過組合資源和HTTP動作實現這一點。我們需要有一種方式來確認是否存在錯誤\/異常:HTTP狀態碼可以實現這一點,可能還會包含闡述它們的響應信息。最好能夠提供API發現和導航的功能:在RESTful中,HATEOAS負責實現這一點。有好的文檔是非常重要的:在這方面,可執行、自更新的文檔可以解決這個問題,這超出了RESTful規範的範圍。最後,但同樣重要的是,優秀的API應該具有緩存功能,除非你的特定情況認爲它是不必要的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST和GraphQL之間最大的區別是它們處理緩存性的方式。當我們使用REST方式構建API的時候,我們基本上可以免費獲得HTTP的緩存功能。如果選擇GraphQL的話,你需要自行負責爲客戶端或應用程序添加緩存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/www.stxnext.com\/blog\/how-to-build-a-good-api-that-wont-embarrass-you"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章