從App角度看API (RESTful) 設計

要API调塌,不要做外星人 (圖片來自網(wǎng)絡)
首發(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設計的一些基本原則

  1. 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,則有好處如下:

  1. 總頁數(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ā)生了另外一件事摔桦,很能說明些問題社付。有個資源文件比較龐大,我采取了如下的使用和更新策略:

  1. 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加載廣告頁。但是廣告頁的展示和動畫則可能有兩種形式:

  1. 廣告頁有navigation bar (push進來或者包在navigation bar中被present出來)洼裤,并顯示navigation title邻辉。
  2. 廣告頁被全屏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)驗總結,可以參考:

  1. HTTP API Design Guide (有中文翻譯)
  2. Best Practices for Designing a Pragmatic RESTful API
  3. API Design Principles (QT的API設計原則--Restful API和Lib API大同小異)
  4. 虛擬研討會:如何設計好的RESTful API试和?(InfoQ的一個總結)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末讯泣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子阅悍,更是在濱河造成了極大的恐慌好渠,老刑警劉巖昨稼,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件携茂,死亡現(xiàn)場離奇詭異抛人,居然都是意外死亡,警方通過查閱死者的電腦和手機点晴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門霍掺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匾荆,“玉大人,你說我怎么就攤上這事杆烁⊙览觯” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵兔魂,是天一觀的道長烤芦。 經(jīng)常有香客問我,道長入热,這世上最難降的妖魔是什么拍棕? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮勺良,結果婚禮上绰播,老公的妹妹穿的比我還像新娘。我一直安慰自己尚困,他們只是感情好蠢箩,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著事甜,像睡著了一般谬泌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逻谦,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天掌实,我揣著相機與錄音,去河邊找鬼邦马。 笑死贱鼻,一個胖子當著我的面吹牛,可吹牛的內容都是我干的滋将。 我是一名探鬼主播邻悬,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼随闽!你這毒婦竟也來了父丰?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤掘宪,失蹤者是張志新(化名)和其女友劉穎蛾扇,沒想到半個月后攘烛,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡屁桑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年医寿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蘑斧。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡靖秩,死狀恐怖,靈堂內的尸體忽然破棺而出竖瘾,到底是詐尸還是另有隱情沟突,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布捕传,位于F島的核電站惠拭,受9級特大地震影響,放射性物質發(fā)生泄漏庸论。R本人自食惡果不足惜职辅,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望聂示。 院中可真熱鬧域携,春花似錦、人聲如沸鱼喉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扛禽。三九已至锋边,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間编曼,已是汗流浹背豆巨。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掐场,地道東北人往扔。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像刻肄,于是被迫代替她去往敵國和親瓤球。 傳聞我的和親對象是個殘疾皇子融欧,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內容