本系列:
- 從零開始的 Android 新專案(1):架構搭建篇
- 從零開始的 Android 新專案(2):Gradle 篇
- 從零開始的 Android 新專案(3):誰告訴你MVP和MVVM是互斥的
- 從零開始的 Android 新專案(4):Dagger2 篇
- 從零開始的 Android 新專案(5):Repository 層(上)
- 從零開始的 Android 新專案(6):Repository 層(下)
- 從零開始的 Android 新專案(7):Data Binding 入門篇
- 從零開始的 Android 新專案(8): Data Binding 高階篇
這回來講講後臺介面的設計。
可能有同學會覺得後臺的介面和我們大前端開發有什麼關係?試想一下,在碰到一些不合理的介面設計的時候,你們開發是否覺得很彆扭——需要為了坑爹的介面寫很多髒程式碼引坑?甚至,這麼開發出來的頁面,體驗也會很差?我們不是說硬無理要求後端介面按照前端業務去封裝,而是說為了專案更好地發展,為了使用者能有更棒的體驗,應該有討論商量的空間。一些差勁的設計,應該被拒絕。
本文使用前端來指代 Android、iOS 以及 Web。
本文不是教大家撕逼的(趕緊撇清關係)。
全域性
全域性指所有介面統一的規範。
請求頭
應該使用http header來放置通用性的引數,比如:
- APPID(Android/iOS/H5)
- APPVER(版本號)
- APP-BUILD-NUM(內部小版本號)
- TOKEN
- NETWORK(網路環境)
- LANGUAGE(語言)
- 等等
前端使用 POST
鍵值對方式提交給後端,可以使用 RawJSON
格式。
Content-Type
設為 application/x-www-form-urlencoded
或者 application/json
。
全域性響應格式
響應格式應該統一,方便前端做統一的處理,尤其是資料欄位,應該統一放在一個map裡面。
名字 | 型別 | 詳細描述 |
---|---|---|
status_no | INT | 狀態碼 |
status_msg | STRING | 狀態資訊 |
data | MAP | 響應內容 |
time | INT | 響應時間戳 |
狀態碼
全域性應該定義統一的狀態碼(status_code),而不應該每個介面單獨去定義。
具體規則可以自行定義,比如0為正確,負數為錯誤。
常見的錯誤狀態碼有
- 普通異常
- token不合法,需要重新登入
- 重複登入
- 需要完善個人資訊
- 第三方賬號登陸,需要繫結官方賬號
- 請求頭不合法(版本號,APPID等)
- 資料解密錯誤
可以根據錯誤型別劃分使用的區域段,如登陸系列使用 -1000 到 -1999 區域。
如此定義後,前端可以進行全域性的統一處理,如重複登陸則踢出使用者。
錯誤資訊
除了特殊的錯誤資訊——如重複登入、token不合法這些狀態碼對應的,以及無網、沒資料這些,對於通用的異常,應該由後臺返回錯誤資訊。
統一data欄位
data 欄位應該統一放在一個 map 內,裡面存放具體的響應資訊。
Scheme
全域性定義統一的 scheme(Deeplink),方便前端進行跳轉。
前端只需要定義自己唯一的 Deeplink 並進行註冊即可(scheme 和 host)。
具體使用 REST 風格(如 markzhai://article/XXX),還是普通的 urlencode (如 markzhai://article/?id=XXX&redirect_url=XXX)可以根據自身需求定義。
使用 REST 風格的一個顧慮是可能 scheme 本身並不是基於資源的,而是基於型別、行為等,所以 urlencode 可能更通用,但相應地基於 Deeplink 的資源索引會希望你是無狀態的 REST 風格。
回傳 or 狀態碼
應該使用回傳還是狀態碼呢?比如點贊訊息,是應該回傳一個 status_code,0則表示點贊成功,還是應該回傳現在的贊狀態呢?
其實這兩者對於後臺的效能來說,是幾乎沒有影響的,因為取得的只是修改的欄位的最後結果。但是對前端來說,差別就有了——需要維護狀態。
舉一個例子:
A 和 B 是兩個使用者,B 關注了 A,A 沒有關注 B。
A 看 B 的主頁的時候,顯示關係是 未關注,此時 A 點選了關注,如果沒有回傳資訊,那麼我們只能把關係重新整理為 已關注,而沒有足夠的資訊去重新整理為 互相關注。否則就需要前端去做噁心的邏輯(後端一開始使用者關係就需要傳 B 關注了 A),根據原來的關係去做切換,還要在失敗的時候刷回原來的狀態。
一些有豐富經驗的後端會在這種介面使用回傳,因為他們知道區別。
模組vs頁面
在後臺的介面設計上,又分為了按頁面以及按模組。
按頁面的介面儘可能讓前端一個頁面只請求一次,一次返回所需要的全部資訊;按模組的介面在後端定義自己的業務模組如使用者、Feed、標籤、搜尋等,並儘量避免模組間的耦合。
從後端角度來說,按模組當然是更好的(只需要劃分地夠細就好),到時候需求有什麼變更,讓前端自己去改變介面的組合就好,自己高枕無憂。但從前端的角度來說,介面的組合涉及到非同步之間的關係,儘管RxJava這樣的響應式程式設計框架讓非同步簡單了很多,但仍然希望可以避免,更嚴重的是,多次介面請求會讓前端的體驗變差,並行介面的影響稍小,而一些有前置後置關係的介面則麻煩比較大,一個接著一個請求,會讓使用者等很久。即便是並行介面,有時候頁面的渲染仍然需要所有介面資料返回後才可以進行。
但如果讓後端按照頁面去套,這樣在後端其實一樣有效能的損耗,需要一個頁面介面去單獨呼叫各個模組的介面,然後進行組合。
究竟如何選擇呢?筆者認為在伺服器效能足夠的前提下,後端應該儘量減少頁面請求次數,尤其是有依賴關係的序列請求。另一方面,在一些影響不那麼大的頁面,則可以由前端自行進行介面組合(比如上面是使用者主頁的使用者展示,下面是該使用者的 feed 列表)。
另外,如果你們有一個好的設計師,那麼他應該會貫徹一個地方只應該以一樣東西為主體,而不應該去把亂七八糟的東西拼湊在一起。
分頁資訊
現代的前端互動上,已經很少會有頁碼顯示了,所以很多後端的列表頁介面中,就沒有帶上了分頁的資訊,而改讓客戶端去維護請求的頁碼。
那麼,分頁資訊在介面中,真的就沒有存在的必要了嗎?其實未必。
為什麼需要分頁資訊
頁面大小(pageSize)可能改變(無論是前端自己的配置亦或是後臺修改),如果僅由客戶端維護頁碼,那麼下次請求下一頁就會出錯,除非客戶端帶上自己上次的頁面大小。
如果客戶端不知道當前頁碼和總頁數,就無法在請求完判斷底部應該顯示上拉載入更多還是沒資料了,導致必須再請求一次,根據是否返回 list 以及資料是否為空去進行判斷。
另外,由後端返回頁碼也避免了客戶端修改頁碼出錯的可能。
但對後端來說,這些資訊的獲取卻意味著更大的計算和I/O資源損耗。
折中辦法
折中地,可以讓後端返回一個 has_more
欄位,這樣可以避免最後一次不必要的請求(尤其是資料都不夠顯示滿一頁的情況下),體驗會好很多。儘管這樣仍然無法避免頁面大小改變的問題。
配置
一些後臺喜歡讓讓前端寫限制邏輯,比如搜尋的關鍵字限制,各種過濾邏輯。
我們們先不提讓前端寫死這些邏輯的靈活性問題(客戶端和網頁不同,不能那麼方便地發版本,即便是網頁,改程式碼發版本就不用測試了嗎?出了問題你背?)。前端的輸入真的可以信任嗎?且不談程式碼可能寫的不夠嚴謹導致輸入跳過了檢查,使用者還能root、越獄,甚至可以反編譯客戶端或者直接模擬請求。
所以良好的配置檢查應該有兩種
- 後端下發配置欄位,前端根據欄位去做對應檢查。好處是減少後臺壓力,壞處是無法保證安全性。
- 後端收到請求自行檢查過濾,如果出錯則返回錯誤資訊給前端顯示。
毋庸置疑,後者更好。
另外,再說說靈活性。今天可能限制3個字,明天產品需求可能就是4個字,現在產品/運營說不會改,到時候難道就真的一定不會改嗎?
空欄位
一些空欄位,如果沒有,服務端應該返回一個空的預設欄位 比如 String 用””,int 用 0,Object 用 {},Array 用 [],這樣減小前端校驗某些校驗漏了出現錯誤的情況(由三帥泥阿布補充)
我個人認為這樣對流量損耗不大,且確實避免了很多可能的異常,是個很好的意見。當然了,正如後端不應該相信前端的輸入一樣,前端也不能相信後端資料的完備性,仍然還是需要悲劇地去校驗。
教訓
- 不要相信什麼以後重構,介面現在這麼說,以後他會告訴你,沒法相容老版本所以只能這樣了(甚至搞出兩套規則讓你同時相容)。
- 不是說後端就是老大。大家的目標都是為了專案能做好,而現在通常前端的壓力比後端更大(前端寫得頭昏腦花,後端網上東逛西逛),所以在不會很大影響效能的前提下,應該滿足前端的合理需求。體驗為先。(硬氣一點,老大應該挺你,甚至親自去撕逼,大不了找CTO)
- 介面的頻繁修改要向上反饋,測試資料不滿足要求也要及時提出。我們們不做背鍋俠。
- 靈活,靈活。做各種需求的時候,想一想,這兒會不會改變?就算現在不會變,以後就不會變嗎?比如抽屜裡的入口,是不是要做成可配置的?多問問,實現上儘量靈活。
總結
本篇講了很多通用的後端介面設計問題。幫助大家在面對一些不合理的介面設計時,能進行友善的討論(撕逼),讓專案能做得更好。歡迎各位在評論裡或者通過郵件(zhaiyifan56@gmail.com)補充其他點,我會標註出來源。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式