首發(fā):http://www.reibang.com/p/ace1428888ca
做了10多年的桌面和邏輯模塊的開發(fā)娄琉,兩年前才開始接觸互聯(lián)網(wǎng)這一塊役听,說起來對RESTful API是沒有太多經(jīng)驗的。
公司app搭建之初一铅,前后端通力合作陕贮,期間同不少后端同事就API的設計都有過溝通交流;到現(xiàn)在app上線也要快滿一年了潘飘,不久前進行了一次大改版肮之,部分API也從v1升級到了v2,覺得有些經(jīng)驗卜录,可以總結一下戈擒。
一. API設計的一些基本原則
-
API是自述的
大多數(shù)我看的資料所寫的Intuitive(直觀的、直覺可理解的)艰毒,也有些寫Descriptive(描述性的)筐高,我將其命名為“自述”。一個API有自述性丑瞧,也就是說看到API的URL柑土,就知道這個API是要干嘛;且這個API的返回值中的字段绊汹,又能很好的解釋其返回的內容稽屏。
雖然API文檔是不可或缺的,但是如果看到API的URL和API的返回值字段就知道這個API的功能作用灸促,多好诫欠!
(注:下文有實例)
API是完備的
對于一組API,我們會要求其為最小完備集浴栽。
對于一個API荒叼,個人覺得其同樣有最小完備性。這里我主要是指在各種輸入?yún)?shù)情況下典鸡,API的返回都應該是合理的被廓、完全的。(實際工作中我發(fā)現(xiàn)為了滿足各種不同的需求萝玷,有時候API的返回值中會插入一下冗余信息嫁乘,因此我這里只提完備性。)
(注:下文有實例)API是抽象的
在軟件工程中球碉,一直都有各式各樣的Add a Layer of Indirection蜓斧,即通過一層抽象,屏蔽掉具體的數(shù)據(jù)/實現(xiàn)/細節(jié)睁冬。使用領域和表現(xiàn)形式各異挎春,其原理實則相同。
API的設計同樣適用。
(注:下文有實例)API是兼容及可擴展的
一個API可能需要同時服務于不同的平臺:Web直奋、iOS能庆、Android等,也可能需要服務同一個平臺的不同版本脚线。雖然可以通過創(chuàng)建不同的API版本(Versioning)達到相同的目的搁胆,但是如果同一個API就能做到,豈不更好邮绿?
(注:下文有實例)其它
另有很多重要特性渠旁,比如安全性、Tracking等船逮,但是我個人覺得和我想表達的主題還是有差異一死。如需更多了解,請參看文章最后的鏈接傻唾。
二. 我親歷、驗證了的API設計Tip
1. 使用 JSON Object (Dictionary/HashMap)
JSON格式簡美承耿,是現(xiàn)在流行的通信格式冠骄。上一句是廢話,其實我想說的是加袋,相對于Array凛辣,使用Object (Obj-C/Swift中可與Dictionary互轉,Java中可與HashMap互轉)
能夠讓API有更大的騰挪空間职烧。
比如有個搜索視頻關鍵字扁誓,返回視頻列表的API,要求能夠讓客戶端對視頻列表進行分頁瀏覽蚀之。最開始設計返回的是JSON Array蝗敢,在
HTTP header
中帶入總頁數(shù)
供客戶端進行分頁處理。
這個API的設計簡明直白:你需要列表足删,我返回Array寿谴。因此服役了不短的時間。
后來需求變了失受,要求在用戶搜索特定關鍵字或者出現(xiàn)特定視頻的時候讶泰,在頁面上加入特殊的Label。然后拂到,然后...不得不重新設計了v2版本的API痪署。為了繼續(xù)服務老版app,后臺需要維護兩套API兄旬。煩狼犯!
若最開始這個API就設計成JSON Object,則有好處如下:
總頁數(shù)
不必帶在HTTP header
中,整個API的信息都集中在Object內辜王。即是我上面提到的“API的自述性”
- 對于新的需求劈狐,增加一對
Key-Value
即可,老版app和新版app采用同一個API呐馆,不需要額外的邏輯去維護兩套API肥缔。即是我上面提到的“API的兼容和擴展性”
2. API不要返回后臺數(shù)據(jù)庫的index(如自增長ID)
前端對后臺資源進行引用時,常需要一個唯一標識汹来,比如xxID之類续膳。當時后臺小伙極力說服我使用數(shù)據(jù)庫中的自增長ID,被我否決了收班。
一般而言坟岔,生成一個全局唯一的UUID或者標識性String都是不錯的選擇。
前些時則發(fā)生了另外一件事摔桦,很能說明些問題社付。有個資源文件比較龐大,我采取了如下的使用和更新策略:
- app內用
本地文件
的方式預存一份數(shù)據(jù) (version=1)
- 當app內需要用到這個數(shù)據(jù)時邻耕,先查找
緩存
鸥咖,再查找本地文件
,這樣可以保證以最快的速度獲取到數(shù)據(jù)進行展示- 同時兄世,app走API向后臺獲取這個文件的新版本(帶入version=1的參數(shù))
- 若數(shù)據(jù)沒有更新的版本啼辣,后臺返回空
- 若數(shù)據(jù)有新版本,則下載并
緩存
這種方法對數(shù)據(jù)量大御滩、更新不頻繁鸥拧、后臺對數(shù)據(jù)容忍性大的API非常適用。
可惜上線前突然bug了削解。最后查找原因富弦,發(fā)現(xiàn)是因為之前都在測試服務器上測試,本地文件
保存的數(shù)據(jù)中含有后臺數(shù)據(jù)庫的自增長ID氛驮。上線前在production服務器上一跑舆声,查無此人。
上面說了一個不使用后臺數(shù)據(jù)庫自增長ID的具體例子柳爽。也即是前面提到的“API的抽象性”媳握。
3. API獲取資源要精準完備
由于業(yè)務邏輯的需要,常規(guī)的API設計可能會有疏漏時磷脯,需要根據(jù)情況仔細斟酌蛾找。
比如有個網(wǎng)站搜集了過去一年和未來半年全世界所有的公開課、講座和會議信息赵誓,當用戶進行瀏覽時打毛,默認顯示當前時間點以后的50條信息柿赊;當用戶往下翻至第50條時,繼續(xù)加載后50條幻枉;當用戶往上翻至第一條并pull整個列表時碰声,前向加載過去的50條信息。
初看起來這個API和前面提到的視頻列表很相似熬甫,但是存在如下條件:
- 用戶每次刷新時胰挑,都有可能有新的公開課、講座或會議成為過去時椿肩,但是用戶在連續(xù)刷新的過程中瞻颂,應該盡量看到完整的(無缺失且不重復)的信息
- 同一時間可能有多個公開課同時開始
- 后臺數(shù)據(jù)在不停添加,有可能在用戶的某兩次刷新間隔郑象,就有了新的數(shù)據(jù)贡这。
因此,傳統(tǒng)的分頁方式肯定是不行的厂榛。中間構思過以第一次瀏覽時間點
作為基準的設計盖矫,也被找出了n多問題。
最終我們采用的是以
UUID
為基準的設計击奶。
- 當用戶第一次瀏覽時炼彪,會根據(jù)用戶的訪問時間點,對所有條目進行時間+自增長ID的二次排序正歼,并返回前50條
- 當用戶向下翻頁時,提供最后一個條目的
UUID
作為參數(shù)拷橘,后臺搜索到其時間和自增長ID局义,然后同樣以時間+自增長ID作為過濾條件進行排序,并返回前50條- 當用戶向前翻頁時冗疮,提供第一個條目的
UUID
作為參數(shù)萄唇,同上。
舉這個例子术幔,主要是想說設計API一定要仔細謹慎另萤,使其具有完備性。但是诅挑,讓人遺憾的是四敞,這個API其實在某些情況下還是會有遺漏,對照前面的3個條件拔妥,你能發(fā)現(xiàn)問題嗎忿危?
4. API默認值的意義
iOS平臺編程中,UIView
類提供了hidden
屬性没龙,用于隱藏此窗口铺厨。為何用hidden
而不用shown
呢缎玫?因為窗口默認是顯示的,對應著屬性的默認值NO(false)解滓。
這樣的設定同樣適用于API的設計赃磨。
比如app中采用統(tǒng)一的廣告策略:使用webview加載廣告頁。但是廣告頁的展示和動畫則可能有兩種形式:
- 廣告頁有navigation bar (push進來或者包在navigation bar中被present出來)洼裤,并顯示navigation title邻辉。
- 廣告頁被全屏present出來
絕大多數(shù)情況下,廣告頁都采用第一種方案展示逸邦,但是特殊的廣告頁可能會要求采用第二種方案展示(比例較低)恩沛。
后臺API提供廣告URL,以及展示廣告頁的形式缕减。
這個例子中雷客,顯示navigation bar的廣告頁就是默認情況,因為顯示navigation bar的概率大桥狡。
在我明了上面這段分析之前搅裙,API是這么設計的:
{ URL: "https://xxx", nav_title: "NOT Ads" } // 糟透了
這里,nav_title
承擔了雙重責任:
- 如果nav_title不為空字符串裹芝,廣告頁顯示navigation bar并設置navigation title部逮。
- 否則,廣告頁采用全屏展示嫂易;
初看起來這樣的設計好像也挺不錯兄朋。但是由于nav_title
的默認值(nil)并不對應廣告頁的默認展示形式(帶navigation bar),可能無法應對新的需求變更怜械。
比如以后因為要加入新的展示形式而需要廢棄掉nav_title
颅和、替換成別的字段時,會發(fā)現(xiàn)nav_title
刪不得缕允。WHY峡扩?因為老版本的用戶必須依賴于這個參數(shù)的非默認值,簡直太糟糕了障本。
至于如何設計這個API才算好教届,那就見仁見智了。
5. 全局HTTP header
全局HTTP頭很金貴驾霜,因為一旦設置案训,則所有的API都會帶上,增加數(shù)據(jù)量+消耗流量粪糙。一般來說萤衰,用戶信息、平臺和版本信息等都是不可缺少的猜旬,更多的可以參看我后面給出的鏈接脆栋。
我這里要提醒的是倦卖,現(xiàn)在的App設計中,總難保不打開一些web頁面椿争,最好記得在webview的HTTP header中做同樣的處理喲~
6. 常量Key
上文提到API返回最好是一個JSON Object (Dictionary/HashMap)
怕膛,便于擴展。本節(jié)標題里說的Key秦踪,就是鍵值對(key-value)中的key
褐捻,而常量就是我們通常意義上的const
。也就是說椅邓,我們用于通信的API接口柠逞,其key最好能寫成const。
比如app請求后臺的廣告業(yè)務數(shù)據(jù)景馁,參數(shù)是placementID板壮,后臺根據(jù)配置,獲取該placementID所對應的廣告展示類型adType合住,然后把數(shù)據(jù)adData返回給app绰精。
一種意見是用placementID做為key,客戶端根據(jù)placementID來獲取廣告透葛,感覺十分直接笨使。結構如下:
{"ads" : {placementID: adData}}
但是app端在這里需要額外的判斷adData所隱含的展示類型,認為不妥
另一個種意見是以廣告的展示類型adType作為key僚害,app端根據(jù)UI的展示方式獲取數(shù)據(jù)硫椰,貼近實現(xiàn)。結構如下:
{"ads" : {adType: adData}}
但是這種方法一是丟掉了placementID的信息萨蚕,擴展性上存在問題(比如一次性請求多個placement的廣告時)靶草;二是廣告的展示類型可能會隨業(yè)務變化,作為key時同樣有兼容性的問題门岔。
但是按照本節(jié)的標題所建議的設計方式就不存在這方面的問題了。結構如下:
{"ads" :
[
{"ad_placement_id": placementID,
"ad_type": adType,
"ad_data": adData},
...
]
}
在上面最后的實現(xiàn)中烤送,所有的key都是const寒随,即"ads"
, "ad_placement_id"
,"ad_type"
和"ad_data"
,整個API都很容易擴展帮坚,保持良好的兼容性妻往。
三. 別人總結的經(jīng)驗
網(wǎng)上也有一些經(jīng)驗總結,可以參考:
- HTTP API Design Guide (有中文翻譯)
- Best Practices for Designing a Pragmatic RESTful API
- API Design Principles (QT的API設計原則--Restful API和Lib API大同小異)
- 虛擬研討會:如何設計好的RESTful API试和?(InfoQ的一個總結)