兵馬未動(dòng),糧草先行。在一款A(yù)PP產(chǎn)品的各個(gè)版本迭代中,兵馬的啟動(dòng)指的是真正開(kāi)始敲代碼的時(shí)候,糧草先行則是指前期的需求,交互,UI等評(píng)審準(zhǔn)備階段,還有本文要說(shuō)的接口的設(shè)計(jì)與評(píng)審。雖然很多時(shí)候一個(gè)api接口的業(yè)務(wù),數(shù)據(jù)邏輯是后端提供的,但真正使用這個(gè)接口的是客戶(hù)端,一個(gè)前端功能的實(shí)現(xiàn)流程與邏輯,有時(shí)候只有客戶(hù)端的RD才清楚,從某種意義來(lái)說(shuō),客戶(hù)端算是接口的需求方。所以建議在前期接口設(shè)計(jì)和評(píng)審時(shí),客戶(hù)端的RD應(yīng)該更多的思考和參與,什么時(shí)機(jī)調(diào)什么接口?每個(gè)接口需要哪些字段?數(shù)據(jù)含義怎么給?只有這些都考慮清楚,且達(dá)成一致并產(chǎn)出接口文檔后,當(dāng)項(xiàng)目真正啟動(dòng)時(shí),根據(jù)接口協(xié)議進(jìn)行開(kāi)發(fā),才能盡量避免各種不確定因素對(duì)項(xiàng)目整體進(jìn)度的影響。本文介紹了接口設(shè)計(jì)中常見(jiàn)的規(guī)范,以及個(gè)人的一些思考與總結(jié),水平有限,權(quán)當(dāng)是拋磚引玉,如果有更好的設(shè)計(jì),請(qǐng)?jiān)谖恼孪路搅粞愿嬖V我,謝謝。
接口設(shè)計(jì)規(guī)范
一. 接口示例
以下是一個(gè)用戶(hù)信息接口的文檔示例,包含接口描述,請(qǐng)求參數(shù),響應(yīng)參數(shù),json示例等。
接口描述:用戶(hù)登陸成功后,或進(jìn)入個(gè)人中心時(shí)會(huì)獲取一次用戶(hù)信息
URI 方法 /userinfo GET 請(qǐng)求參數(shù)
名稱(chēng) 必填 備注 id 是 用戶(hù)id 響應(yīng)參數(shù)
名稱(chēng) 類(lèi)型 備注 id String 用戶(hù)id name String 姓名,例:張三 age String 年齡,例:20 json示例
{ "code":200, "msg":"成功", "time":"1482213602000", "data": { "id":"1001", "name":"張三", "age":"20" }}二. 基本規(guī)范
1.通用請(qǐng)求參數(shù)
每個(gè)請(qǐng)求都要攜帶的參數(shù),用于描述每個(gè)請(qǐng)求的基本信息,后端可以通過(guò)這些字段進(jìn)行接口統(tǒng)計(jì),或APP終端設(shè)備的統(tǒng)計(jì),一般放到header或url參數(shù)中。
字段名稱(chēng) 說(shuō)明 version 客戶(hù)端版本version,例:1.0.0 token 登陸成功后,server返回的登陸令牌token os 手機(jī)系統(tǒng)版本(Build.VERSION.RELEAS)例:4.4,4.5 from 請(qǐng)求來(lái)源,例:android/ios/h5 screen 手機(jī)尺寸,例:1080*1920 model 機(jī)型信息(Build.MODEL),例:Redmi Note 3 channel 渠道信息,例:com.wandoujia net APP當(dāng)前網(wǎng)絡(luò)狀態(tài),例:wifi,mobile;部分接口可以根據(jù)用戶(hù)當(dāng)前的網(wǎng)絡(luò)狀態(tài),下發(fā)不同數(shù)據(jù)策略,如:wifi則返回高清圖,mobile情況則返回縮略圖 appid APP唯一標(biāo)識(shí),有的公司一套server服務(wù)多款A(yù)PP時(shí),需要區(qū)分開(kāi)每個(gè)APP來(lái)源 2.請(qǐng)求Path,http://www.online.com/api/ [path]
原則:在以下命名規(guī)范的基礎(chǔ)上盡量保持良好的可讀性,見(jiàn)名知意。另外這里需要額外提下restful規(guī)范,個(gè)人理解restful規(guī)范是通過(guò)path表示當(dāng)前請(qǐng)求的資源,通過(guò)method表示當(dāng)前請(qǐng)求的操作動(dòng)作(post=增,delete=刪,put=改,get=查),例:GET /userinfo/{id},通過(guò)這個(gè)path就可以清楚的知道當(dāng)前請(qǐng)求的意圖是根據(jù)id獲取用戶(hù)信息,而APP開(kāi)發(fā)中很多時(shí)候一個(gè)頁(yè)面是需要同時(shí)獲取,如,用戶(hù),訂單,營(yíng)銷(xiāo)各種信息,這時(shí)候就很難用一個(gè)path來(lái)表示當(dāng)前請(qǐng)求的真正意圖,restful規(guī)范就很難得到實(shí)現(xiàn),有不同見(jiàn)解的歡迎交流。故本文介紹的接口設(shè)計(jì)方法,只區(qū)分get和post,通過(guò)path命名定義請(qǐng)求行為,
操作行為 Method Path 查找 GET getXxx 增加 POST addXxx/submitXxx 修改 POST modifyXxx 刪除 POST delXxx 示例
操作行為 Method Path 獲取用戶(hù)信息 GET getUserInfo 增加收貨地址 POST addAddress 修改密碼 POST modifyPwd 刪除收貨地址 POST delAddress 登陸 GET login 發(fā)送短信驗(yàn)證碼 GET sendSms 訂單支付 POST orderPay 3.響應(yīng)數(shù)據(jù)
字段名稱(chēng) 說(shuō)明 code 響應(yīng)狀態(tài)碼,200:成功;非200:失敗 msg 請(qǐng)求失敗時(shí)的message time 服務(wù)端時(shí)間戳,單位:毫秒。用于同步時(shí)間 data 數(shù)據(jù)實(shí)體 code=200時(shí),msg=登陸成功/修改成功/提交成功;如果需要Toast,可以直接使用msg。code!=200時(shí),msg=錯(cuò)誤提示信息;比如login接口,"賬號(hào)或密碼錯(cuò)誤","賬號(hào)不存在"類(lèi)似這些的業(yè)務(wù)提示文案放在msg字段,客戶(hù)端直接Toast就可以了。不過(guò)需要提醒后端同學(xué),錯(cuò)誤提示不能自己覺(jué)的什么合適就提示什么,要按需求文檔來(lái)提供,或和PM確認(rèn)。
object類(lèi)型數(shù)據(jù)
// json{ "code":200, "msg":"成功", "time":"1482213602000", "data": { "name":"張三", "age":"20" }} // model.javapublic class Model { public String name; public String age;}array類(lèi)型數(shù)據(jù),正常情況下在解析json的時(shí)候,1.先解析code和msg,判斷code==200的情況下繼續(xù)解析data。2.將data下面的json串解析成當(dāng)次請(qǐng)求需要的model數(shù)據(jù)結(jié)構(gòu)。對(duì)于array類(lèi)型的數(shù)據(jù),即使只有1個(gè)list字段,也要保證data下是個(gè)完整的object結(jié)構(gòu),這樣我們?cè)谟肎son解析model的時(shí)候,統(tǒng)一將data層級(jí)下的數(shù)據(jù)當(dāng)object解析就可以了,不用區(qū)分object或array的情況。
// json{ "code":200, "msg":"成功", "time":"1482213602000", "data": { "list":["張三","李四"] }} // model.javapublic class Model { public List<String> list;}array+分頁(yè)類(lèi)型數(shù)據(jù),需要額外返回total字段,客戶(hù)端需要通過(guò)total判斷本地加載的list是否還有更多可以加載。
請(qǐng)求參數(shù)
名稱(chēng) 必填 備注 pageNum 是 當(dāng)前第幾頁(yè),例:1,2,3 pageSize 是 每頁(yè)條數(shù),例:10 響應(yīng)數(shù)據(jù)
// json{ "code":200, "msg":"成功", "time":"1482213602000", "data": { "list":["張三","李四"], "total":"10" }} // model.javapublic class Model { public List<String> list; public String total;}不論列表頁(yè)面是支持分頁(yè)加載,還是一次加載全部數(shù)據(jù),都建議將接口設(shè)計(jì)成支持分頁(yè)的,如果要實(shí)現(xiàn)一次性加載只要把pageSize改成類(lèi)似Integer.Max的值。這樣設(shè)計(jì)的好處是客戶(hù)端和后端可以設(shè)計(jì)一套統(tǒng)一的分頁(yè)列表模版代碼,即使需求變更,也可以很好的支持。
4.命名規(guī)范
統(tǒng)一命名:與后端約定好即可(php和js在命名時(shí)一般采用下劃線(xiàn)風(fēng)格,而Java中一般采用的是駝峰法),無(wú)絕對(duì)標(biāo)準(zhǔn),不要同時(shí)存在駝峰"userName",下劃線(xiàn)"phone_number"兩種形式就可以了。
避免冗余字段:每次在新增接口字段時(shí),注意是否已經(jīng)存在同一個(gè)含義的字段,保持命名一致,不要同時(shí)存在"userName","username","uName"多種同義字段。
注釋清晰(重要):每個(gè)接口/字段都需要有詳細(xì)的描述信息,很多時(shí)候接口體現(xiàn)業(yè)務(wù)邏輯,是團(tuán)隊(duì)中很重要的文檔沉淀,同時(shí),詳細(xì)的接口文檔,可以幫助新人快速熟悉業(yè)務(wù)。具體示例如下:
接口描述:用戶(hù)登陸成功后會(huì)獲取一次用戶(hù)信息,每次進(jìn)入個(gè)人中心也會(huì)重新獲取一遍
URI 方法 /userinfo GET 字段描述:數(shù)值要有單位,時(shí)間要有格式,狀態(tài)字段要有狀態(tài)描述,以及不同狀態(tài)下對(duì)于其他字段返回邏輯的關(guān)聯(lián)關(guān)系。
字段類(lèi)型 字段名稱(chēng) 說(shuō)明 Boolean isVip 是否時(shí)Vip用戶(hù),1:是,0:否 金額 realPay 訂單實(shí)際付款金額,單位:元 時(shí)間 payTime 訂單付款時(shí)間,單位:毫秒 日期 payDate 訂單付款日期,格式"yyyy-MM-dd" 狀態(tài) status 訂單狀態(tài),1:進(jìn)行中(payDate不返回),2:待支付(payDate返回),3:已支付(payDate不返回);(bool以1/0表示,狀態(tài)從1+開(kāi)始) 5.統(tǒng)一定義String字段類(lèi)型
// json{ "name":"張三", "isVip": true, "age":20, "money": 10.5}// Model.javapublic class Model { String name; boolean isVip; int age; float money;}如果使用的是Gson庫(kù)的話(huà),正常情況下這么定義model是可以正常解析,但是會(huì)有以下異常情況:
Boolean型字段{ //如果傳true,false以外的數(shù)據(jù),就會(huì)解析失敗 "isVip": 20 "isVip": }解析報(bào)錯(cuò):
(1)java.lang.IllegalStateException: Expected a boolean but was NUMBER(2)com.google.gson.stream.MalformedJsonException: Unexpected valueInt類(lèi)型字段{ "age": 20.5 "age": abc "age": "" "age": }解析報(bào)錯(cuò):
(1)java.lang.NumberFormatException: Expected an int but was 20.5(2)java.lang.IllegalStateException: Expected an int but was STRING(3)java.lang.NumberFormatException: empty String(4)com.google.gson.stream.MalformedJsonException: Expected valueFloat類(lèi)型字段{ "money": abc "money": ""}解析報(bào)錯(cuò):
(1)java.lang.NumberFormatException: For input string: "abc"(2)java.lang.NumberFormatException: empty StringGson庫(kù)在解析到某個(gè)非法字段時(shí),會(huì)拋出各種異常,導(dǎo)致整個(gè)model的解析失敗。客戶(hù)端沒(méi)處理好的話(huà),會(huì)因?yàn)檫@種時(shí)不時(shí)的臟數(shù)據(jù)引發(fā)各種奇怪的bug。解決方案:
修改Gson源碼,對(duì)于字段解析失敗的異常進(jìn)行捕獲,保證model解析完成,非正常解決方案,修改源碼后Gson庫(kù)就不能隨便更新了,獲取替換其他json解析庫(kù)也變的不方便。自定義JsonDeserializer,比較正常的解決思路。public class IntegerDefaultAdapter implements JsonDeserializer<Integer> { @Override public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { // 如果integer類(lèi)型的字段,進(jìn)行一次類(lèi)型轉(zhuǎn)換 try { return Integer.parseInt(json.getAsString()); } catch (NumberFormatException e) { } return -1; }}String json = "{name:listen,isVip:true,age:abc,money:1.0}";Gson gson = new GsonBuilder().registerTypeAdapter(int.class, new IntegerDefaultAdapter()).create();Model model = gson.fromJson(json, Model.class);// age字段解析出來(lái)為-1將APP接收數(shù)據(jù)的類(lèi)型定義為容錯(cuò)能力更強(qiáng)的String(推薦)。{ "name": "abc" "name": "20" "name": "10.2" "name": "true"}優(yōu)點(diǎn):
容錯(cuò)性強(qiáng),規(guī)避因臟數(shù)據(jù)引起的數(shù)據(jù)解析失敗。age,money這些字段大部分情況下都是直接展示,此時(shí)便可省去拼接 "",或String.valueOf()等步驟。另外假設(shè)此時(shí)將age字段定義為int類(lèi)型,很容易就會(huì)直接調(diào)用textView.setText(age),那么這個(gè)age就會(huì)當(dāng)成resId去執(zhí)行,導(dǎo)致資源找不到報(bào)錯(cuò),定義為String可以避免此類(lèi)錯(cuò)誤。注意事項(xiàng):
Boolean類(lèi)型數(shù)據(jù),統(tǒng)一返回1(true)和0(false),客戶(hù)端做一層容錯(cuò)判斷,只有1才為true,其他非1,解析失敗的情況均為false,例:
if(!TextUtils.isEmpty(isVip) && "1".equals(isVip)) { return true;} return false;status類(lèi)型字段從1+開(kāi)始,和Boolean類(lèi)型(0否,1是)區(qū)分開(kāi)。"0"的含義有2種,(1)非0即為真,所以0即表示false;(2)"0"是一種未賦值的默認(rèn)狀態(tài)。假設(shè)此時(shí)用0表示狀態(tài)1,那么就很難判斷出到底時(shí)數(shù)據(jù)解析失敗,使用默認(rèn)值0,還是說(shuō)邏輯走通并賦值為0。例:orderStatus,1:進(jìn)行中,2:待支付,3:已完成。
int,float類(lèi)型數(shù)據(jù),如果不是直接展示的話(huà),需要做一次類(lèi)型轉(zhuǎn)換,注意捕獲異常,在解析失敗的情況下,使用default值。
int defaultInt = -1;try {defaultInt = Integer.parseInt(age);} catch (NumberFormatException e) {e.PRintStackTrace();}return defaultInt;6.上傳/下載接口,根據(jù)md5校驗(yàn)數(shù)據(jù)完整性
上傳,下載文件/圖片時(shí),除了file本身,還要攜帶該file的md5,在傳輸過(guò)程中可能丟失部分?jǐn)?shù)據(jù),導(dǎo)致文件損毀,所以需要通過(guò)md5值進(jìn)行完整性校驗(yàn)。上傳成功后,正常情況后端只需要返回code表示成功/失敗,在開(kāi)發(fā)階段,可以讓后端將上傳成功后的圖片url返回,這樣當(dāng)我們調(diào)用完接口以后,就可以通過(guò)該url字段查看圖片是否上傳成功,存儲(chǔ)的尺寸大小,模糊度等,就不用每次粘著后端幫忙看請(qǐng)求結(jié)果了,這個(gè)思路同樣通用于其他接口,不過(guò)上線(xiàn)后需要將這個(gè)不必要的字段去掉。
{ "code":200, "msg":"成功", "time":"1482213602000", "data": { "url":"http://www.online.com/path/pic.jpg" }}7.避免浮點(diǎn)型計(jì)算
浮點(diǎn)型計(jì)算可能導(dǎo)致精度丟失,為了避免,可以縮小單位進(jìn)行存儲(chǔ)。例:1.5元,后端會(huì)以150分存到數(shù)據(jù)庫(kù),1.5km會(huì)存成1500m。同理,如果一個(gè)類(lèi)似距離的字段,如果是展示用,則直接返回"1.5km",如果涉及到邏輯判斷與計(jì)算(如:>1000m,執(zhí)行邏輯A,>1500m,執(zhí)行邏輯B),可以返回"1500,單位(m)",至少比傳1.5來(lái)的方便。當(dāng)然如果要計(jì)算浮點(diǎn)型也是可以的,需要用到BigDecimal,這么設(shè)計(jì)只是為了減少出錯(cuò)的可能性。
8.json數(shù)據(jù)保持良好結(jié)構(gòu)
{ "userId"... "userName"... "userPhoto"... "orderId"... "orderType"... "addressId"... "addressName"... "addressDetail"...}json的3類(lèi)信息user,order,address,全部堆在一起,字段多了以后,對(duì)于接口信息的讀取很不直觀(guān);客戶(hù)端在定義model的時(shí)候,會(huì)將全部字段定義在一個(gè)model中,如果其他地方也有用到addressId,Name,Detail等字段信息,則需要重新定義address的model,無(wú)法實(shí)現(xiàn)model的復(fù)用。
{ "user":{ "id"... "name"... "photo"... } "order":{ "id"... "type"... } "address":{ "id"... "name"... "detail"... } }經(jīng)過(guò)優(yōu)化后user,order,address字段在各自的結(jié)構(gòu)體內(nèi),一眼就可以看出這個(gè)接口有哪些類(lèi)型的數(shù)據(jù)。還有點(diǎn)要注意,如果放在同一級(jí)別,id字段就需要用userId,orderId,addressId區(qū)分開(kāi),而現(xiàn)在根據(jù)不同結(jié)構(gòu)體區(qū)分字段類(lèi)型后,直接使用id就可以了,如果還使用userId,寫(xiě)代碼的時(shí)候就會(huì)出現(xiàn)data.getUser().getUserId()的寫(xiě)法,就會(huì)很奇怪。
三. 瘦客戶(hù)端
眾所周知,客戶(hù)端任何的修改都是需要發(fā)版的,特別是IOS需要走AppStore的審核流程。為了修一個(gè)bug,僅僅改幾行代碼,而重新走一輪發(fā)版流程,是很勞民傷財(cái)?shù)摹K栽诮涌谠O(shè)計(jì)的時(shí)候,也需要適當(dāng)考慮這點(diǎn),將業(yè)務(wù)重心交由后端,客戶(hù)端保持邏輯簡(jiǎn)單。有時(shí)候,一個(gè)功能,客戶(hù)端,后端都可以做,那么為什么客戶(hù)端就是不做,要后段拼好提供呢?還是那句話(huà),后端一天可以發(fā)n個(gè)版,客戶(hù)端一個(gè)版本卻只能發(fā)一次,有些團(tuán)隊(duì)一開(kāi)始并沒(méi)意識(shí)到這點(diǎn),總覺(jué)后端就是重度業(yè)務(wù)邏輯的所在,管那么多前端的展示,字符串拼接邏輯干嘛,可是,真正到了出問(wèn)題(bug或需求變更)需要發(fā)版的時(shí)候,雖然70%的鍋是客戶(hù)端背,但是,剩余30%也會(huì)對(duì)當(dāng)初重客戶(hù)端的選擇而后悔,不過(guò)重點(diǎn)不是誰(shuí)背鍋,而是產(chǎn)品不出問(wèn)題。so,為了大局,后端的RD們,我們得聊聊。
客戶(hù)端盡量只負(fù)責(zé)展示邏輯,不處理業(yè)務(wù)邏輯
例如:客戶(hù)端有個(gè)TextView,后端只給個(gè)status字段,status=1時(shí),展示文案1;status=2時(shí),展示文案2;這樣設(shè)計(jì)的缺點(diǎn)是,如果以后要修改status=3時(shí),展示文案1,那么這個(gè)status判斷邏輯時(shí)寫(xiě)死在客戶(hù)端,就沒(méi)辦法支持這種修改,且這種設(shè)計(jì)限定死了TextView只能展示2種文案。推薦方案是后端直接將TextView需要展示的文案下發(fā),這樣不管是status的判斷,還是文案的展示,后期都是可變的。
客戶(hù)端不處理金額的計(jì)算
例如:外賣(mài)APP,用戶(hù)在下單的時(shí)候,需要選擇收貨地址,支付類(lèi)型,優(yōu)惠券等,任何一個(gè)選項(xiàng)的修改,都可能影響用戶(hù)最后需要支付的金額。所以這里比較常見(jiàn)的接口設(shè)計(jì)是在每次選擇完回到訂單支付頁(yè)面后,再發(fā)送一次請(qǐng)求,后端根據(jù)當(dāng)前選項(xiàng)重新計(jì)算金額。金額永遠(yuǎn)是一款產(chǎn)品最重要,最敏感的信息,如果交由客戶(hù)端計(jì)算,萬(wàn)一出錯(cuò),即使少1分,都是毀滅性的,所以,關(guān)于金額,展示就好。
客戶(hù)端少處理請(qǐng)求參數(shù)的校驗(yàn)與約束提示
例如:修改密碼功能,密碼規(guī)則"6-12字母,數(shù)字,下劃線(xiàn)",有3種做法:
在發(fā)送請(qǐng)求前,客戶(hù)端校驗(yàn)密碼規(guī)則,如果不符合,則不發(fā)送請(qǐng)求。優(yōu)點(diǎn):規(guī)則不滿(mǎn)足時(shí),可以減少不必要的請(qǐng)求。缺點(diǎn):客戶(hù)端寫(xiě)死校驗(yàn)邏輯,密碼規(guī)則變化時(shí),客戶(hù)端需要發(fā)版。客戶(hù)端只判斷null,和最短位數(shù)限制,其他校驗(yàn)規(guī)則交由后端處理。優(yōu)點(diǎn):靈活性最好。缺點(diǎn):后端壓力大,校驗(yàn)請(qǐng)求多。后端在通用配置的接口返回正則表達(dá)式,客戶(hù)端獲取后進(jìn)行正則校驗(yàn)。優(yōu)點(diǎn):具有一定靈活性。缺點(diǎn):開(kāi)發(fā),調(diào)試成本較高。(推薦:即使出問(wèn)題,也可以清除配置,回退到第2個(gè)方案)四.擴(kuò)展性
接口的設(shè)計(jì)要具有一定的擴(kuò)展性,考慮到后續(xù)版本變化,對(duì)于接口,字段的影響及變化。
文案與圖片
對(duì)于界面上的文案,圖片,特別是"xxx20分鐘之內(nèi)","xxx7天到期"這些帶數(shù)字的文案,不可能永遠(yuǎn)不變的,即使和PM確認(rèn)了打死不變,也最好通過(guò)常量配置接口進(jìn)行下發(fā)(未下發(fā)時(shí)使用APP本地默認(rèn)文案,下發(fā)時(shí)使用下發(fā)的文案),我們的原則是:變與不變都能支持。
數(shù)據(jù)列表化:盡量用List(key, value)的數(shù)據(jù)格式定義類(lèi)似列表的界面
list.png
方案1:客戶(hù)端在寫(xiě)xml的時(shí)候?qū)⒆髠?cè)的"姓名","性別","年齡"寫(xiě)死,右側(cè)的具體數(shù)據(jù)從json解析獲得
{ "name": "張三", "sex": "男", "age": "20歲", "nickName": "小張"}方案2(推薦):將左側(cè)的title和右側(cè)的value,以list(key-value)的數(shù)據(jù)形式進(jìn)行下發(fā),優(yōu)點(diǎn):左,右側(cè)文案靈活配置,后期如果需要擴(kuò)展,新增或刪除一個(gè)條目,都可以通過(guò)后端控制。不過(guò)采用這種形式,也需要考慮實(shí)際場(chǎng)景,對(duì)于變化不那么頻繁,數(shù)據(jù)item較少,較固定的情況下其實(shí)沒(méi)有必要設(shè)計(jì)的太靈活,只會(huì)增加開(kāi)發(fā)成本。
{ "userInfos":[ { "key":"姓名", "value":"張三" },{ "key":"性別", "value":"男" },{ "key":"年齡", "value":"20歲" },{ "key":"昵稱(chēng)", "value":"小張" }]}3.用flag替換boolean:一般情況下,一款A(yù)PP都會(huì)有config接口,用于獲取一些常量文案,通用配置等信息,會(huì)有很多類(lèi)似開(kāi)關(guān)的字段,如:"isNew","isVip","isShowBalance"等等。
{ "isNew":"1",// 是否是新用戶(hù) "isVip":"1",// 是否是VIP用戶(hù) "isShowBalance":"1",//是否顯示側(cè)邊欄余額模塊}優(yōu)化方案:通過(guò)二進(jìn)制第1位表示"isNew",二進(jìn)制第2位表示"isVip",二進(jìn)制第3位表示"isShowBalance"。如果有其他新增狀態(tài),不需要新增字段,就需要改變返回的數(shù)據(jù)即可。
{ "flag":"7"// 二進(jìn)制:111,表示3個(gè)狀態(tài)都為true "flag":"5"// 二進(jìn)制:101,表示isNew,isShowBalance為true,isVip為false}long flag = 5;System.out.println("bit=" + Long.toBinaryString(flag));System.out.println("isNew=" + ((flag & 1) == 1));System.out.println("isVip=" + ((flag & 2) == 2));System.out.println("isShowBalance=" + ((flag & 4) == 4));bit=101isNew=trueisVip=falseisShowBalance=true五.安全性
響應(yīng)數(shù)據(jù)中包含用戶(hù)隱私的字段數(shù)據(jù),需要加*號(hào)。如:手機(jī)號(hào),身份證,用戶(hù)郵箱,支付賬號(hào),郵寄地址等。
{ "phone":"150*****000", "idCard":"3500**********0555", "email":"40*****00@QQ.com" }請(qǐng)求參數(shù)中包含用戶(hù)隱私的字段參數(shù),如:登陸接口的密碼字段,需要進(jìn)行加密傳輸,避免被代理捕捉請(qǐng)求后獲取明文密碼。
客戶(hù)端和服務(wù)器通過(guò)約定的算法,對(duì)傳遞的參數(shù)值進(jìn)行簽名匹配,防止參數(shù)在請(qǐng)求過(guò)程中被抓取篡改。密鑰記得放到so中,放在java層太不安全,so中要進(jìn)行keystore反向簽名校驗(yàn),避免so被獲取后直接調(diào)用獲取算法。
so中要進(jìn)行keystore反向簽名校驗(yàn)
Java層在進(jìn)行參數(shù)簽名計(jì)算的時(shí)候需要獲取app本地存儲(chǔ)的密鑰,調(diào)用NativeHelper.getKey(),在so中通過(guò)反射調(diào)用java層的getSignature(),比較是否和so中存儲(chǔ)的keyStore哈希值一致,如果是則返回密鑰,不是則返回空字符串。
Java層的NativeHelper.java
package com.listen.test; public class NativeHelper { static { System.loadLibrary("native-lib"); } // 調(diào)用so獲取密鑰 public native String getKey(); // 獲取當(dāng)前keyStore的hash值 public String getSignature() { final String packname = Baseapplication.getInstance().getPackageName(); PackageInfo packageInfo; try { packageInfo = BaseApplication.getInstance() .getPackageManager() .getPackageInfo(packname, PackageManager.GET_SIGNATURES); Signature[] signs = packageInfo.signatures; Signature sign = signs[0]; return sign.hashCode() + ""; } catch (Throwable t) { if (null != t) { t.printStackTrace(); } } return ""; } }so層的native-lib.c
// 字符串轉(zhuǎn)字符 char* _JString2CStr(JNIEnv* env, jstring jstr) { char* rtn; jclass clsstring = (*env)->FindClass(env, "java/lang/String"); jstring strencode = (*env)->NewStringUTF(env, "GB2312"); jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B"); jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode); // String .getByte("GB2312"); jsize alen = (*env)->GetArrayLength(env, barr); jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE); if (alen > 0) { rtn = (char*) malloc(alen + 1); //"/0" memcpy(rtn, ba, alen); rtn[alen] = 0; } (*env)->ReleaseByteArrayElements(env, barr, ba, 0); return rtn; } char* storeKeyHash = "1234567890";// 該值可以通過(guò)java層的getSignature獲取 JNIEXPORT jstring JNICALL Java_com_listen_test_NativeHelper_getKey( JNIEnv *env, jobject obj, jbyteArray array) { // 反射獲取當(dāng)前keyStore的hash值 jclass jClazz = (*env)->FindClass(env, "com/listen/test/NativeHelper"); jmethodID jmethodid = (*env)->GetMethodID(env, jClazz, "getSignature", "()Ljava/lang/String;"); jstring appSign = (jstring)(*env)->CallObjectMethod(env, obj, jmethodid); // 判斷是否是本程序的簽名哈希值 char* charAppSign = _JString2CStr(env, appSign); //將jstring轉(zhuǎn)換為cha* if (strcmp(charAppSign, storeKeyHash) != 0) { return (*env)->NewStringUTF(env, "");//keyStore的hash不一致,不是在當(dāng)前app種調(diào)用該so } return (*env)->NewStringUTF(env, "秘鑰值");//keyStore的hash一致,返回密鑰 }六.兼容性
APP1.0在使用接口A,如果此時(shí)在開(kāi)發(fā)1.1的時(shí)候修改了接口A的邏輯,在1.1發(fā)版的時(shí)候線(xiàn)上就會(huì)出現(xiàn)2個(gè)版本的客戶(hù)端訪(fǎng)問(wèn)同一個(gè)接口A,為了保證1.0客戶(hù)端調(diào)用接口A不會(huì)出錯(cuò),就需要通過(guò)version字段或path中的"v1/login","v2/login"進(jìn)行區(qū)分,不同版本客戶(hù)端訪(fǎng)問(wèn)同一接口時(shí)處理邏輯要各自獨(dú)立。
接口/字段的刪除,修改要謹(jǐn)慎:對(duì)于已經(jīng)存在的接口進(jìn)行修改,需要考慮對(duì)線(xiàn)上版本的影響,盡量是數(shù)據(jù)含義,和新增字段,而不是去修改。
md5緩存的兼容性:如果1.0的接口A存在md5緩存,正常都是后端上線(xiàn)后再發(fā)布1.1客戶(hù)端的順序,如果在后端上線(xiàn)后,1.1還沒(méi)發(fā)布的情況下,此時(shí)1.0的客戶(hù)端就緩存了1.1后端邏輯的md5,在更新成1.1的時(shí)候,md5沒(méi)有變,就有可能緩存的還是1.0的數(shù)據(jù),所以比較推薦后端在計(jì)算md5的時(shí)候把version加上,這樣更新APP可以保證md5是不一樣的。
七.性能優(yōu)化
合并接口
為了減少客戶(hù)端和服務(wù)器建立連接和斷開(kāi)連接消耗的時(shí)間,資源,電量,盡量避免頻繁的間隔網(wǎng)絡(luò)請(qǐng)求。業(yè)務(wù)場(chǎng)景允許的情況下,盡量1個(gè)頁(yè)面對(duì)應(yīng)1個(gè)接口。原先一個(gè)頁(yè)面要通過(guò)多個(gè)請(qǐng)求獲取多種類(lèi)型數(shù)據(jù)的情況,最好能通過(guò)一個(gè)接口全部獲取得到。又如:在調(diào)用B接口前需要A接口的前置數(shù)據(jù)的情況,可以讓后端支持下,在調(diào)用A接口時(shí)直接返回B接口的數(shù)據(jù),減少類(lèi)似這種的連續(xù)請(qǐng)求。
字段精簡(jiǎn)
定義字段名時(shí),在保證良好可讀性的前提下,盡量精簡(jiǎn),減少流量的消耗
{ "orderDescription" >> "orderDesc" "oldPassWord" >> "oldPwd" "longitude" >> "lng" "latitude" >> "lat" }md5緩存
對(duì)于頻繁調(diào)用,且數(shù)據(jù)不常變化的接口(config配置接口),可以在返回的數(shù)據(jù)中添加md5字段(用于校驗(yàn)除md5外其他數(shù)據(jù)是否變化),在下次請(qǐng)求的時(shí)候?qū)⑦@個(gè)md5作為參數(shù)傳給后端,md5沒(méi)有變化的情況下,不返回data,客戶(hù)端可以直接使用上次請(qǐng)求緩存在本地的data。
md5.png
無(wú)用字段清理
每個(gè)版本的接口更新后,需要將無(wú)用字段進(jìn)行清理。或者同個(gè)接口不同狀態(tài)下需要返回的字段各不相同的時(shí)候,當(dāng)次請(qǐng)求不需要的字段需要提醒后端不必下發(fā),避免傳輸無(wú)用數(shù)據(jù)浪費(fèi)用戶(hù)流量。
圖片裁剪服務(wù)
客戶(hù)端上傳圖片后,當(dāng)需要在列表這些圖片區(qū)域較小的地方展示的時(shí)候,沒(méi)必要直接加載原圖,可以先在后端通過(guò)圖片裁剪服務(wù)處理后再進(jìn)行展示。例:http://image-demo.img-cn-hangzhou.aliyuncs.com/example.jpg@100h_100w_1e_1c?spm=5176.doc32223.2.3.jmkKF9&file=example.jpg@100h_100w_1e_1c, 這是阿里云的圖片裁剪服務(wù),在url后面直接拼上裁剪參數(shù),就可以實(shí)現(xiàn)將原圖居中裁剪成100*100的縮略圖。當(dāng)需要展示高清圖的時(shí)候,再加載原圖的url。
局部刷新
一個(gè)頁(yè)面,如果之前已經(jīng)加載了20%的數(shù)據(jù),那么就不需要每次都返回100%數(shù)據(jù),只要返回剩余80%即可。例:訂單列表頁(yè)面,每個(gè)item已經(jīng)具有類(lèi)似orderId,orderDesc等字段,那么點(diǎn)擊進(jìn)入訂單詳情的時(shí)候,orderId,orderDesc就可以從訂單列表傳遞過(guò)來(lái)即可,詳情頁(yè)的請(qǐng)求只需要返回訂單相關(guān)的剩余數(shù)據(jù),客戶(hù)端需要額外處理數(shù)據(jù)組裝邏輯,將前一個(gè)頁(yè)面?zhèn)鬟f過(guò)來(lái)的字段和詳情頁(yè)請(qǐng)求到的字段組裝成完整的model數(shù)據(jù)。
wifi與移動(dòng)網(wǎng)絡(luò)的區(qū)別對(duì)待
WiFi連接下,網(wǎng)絡(luò)傳輸?shù)碾娏肯囊纫苿?dòng)網(wǎng)絡(luò)少很多,應(yīng)該盡量減少移動(dòng)網(wǎng)絡(luò)下的數(shù)據(jù)傳輸,多在WiFi環(huán)境下傳輸數(shù)據(jù)。例:crash日志上報(bào),數(shù)據(jù)統(tǒng)計(jì)接口等,可以在移動(dòng)網(wǎng)絡(luò)的情況下請(qǐng)求頻率降低,或緩存,在wifi網(wǎng)絡(luò)時(shí)上調(diào)請(qǐng)求頻率,或?qū)⒕彺娴臄?shù)據(jù)統(tǒng)一上報(bào)。還有上文提到的,如果是wifi網(wǎng)絡(luò)狀態(tài)下,就下發(fā)高清圖提升用戶(hù)體驗(yàn),移動(dòng)網(wǎng)絡(luò)狀態(tài)就下發(fā)縮略,或裁剪圖。
八.體驗(yàn)優(yōu)化
設(shè)計(jì)接口時(shí),不能只考慮減少流量消耗,性能優(yōu)化等,特定場(chǎng)景下用戶(hù)體驗(yàn)的優(yōu)化才是最高優(yōu)先級(jí)的。
通過(guò)預(yù)加載降低對(duì)網(wǎng)絡(luò)的依賴(lài)
使用APP的場(chǎng)景為網(wǎng)絡(luò)較差的情況。例:配送員在使用配送APP的時(shí)候,商家地址如果在地下室,或配送員進(jìn)入電梯的時(shí)候,這時(shí)候常要查看訂單詳情,網(wǎng)絡(luò)信號(hào)又比較差的,就會(huì)影響正常工作。可以考慮在訂單列表的接口中,將訂單詳情的數(shù)據(jù)一起請(qǐng)求下來(lái),并通過(guò)md5判斷詳情頁(yè)面數(shù)據(jù)是否變化,避免重復(fù)加載,這樣其實(shí)用戶(hù)在網(wǎng)絡(luò)比較好的情況下請(qǐng)求一次列表后,再進(jìn)入詳情頁(yè),就不再需要重新請(qǐng)求,對(duì)網(wǎng)絡(luò)的依賴(lài)也是最小的。同理,對(duì)于一些閱讀類(lèi)APP,前幾頁(yè)的文章,用戶(hù)查看詳情的概率較高,可以在返回文章列表的時(shí)候攜帶正文內(nèi)容,則可以實(shí)現(xiàn)秒開(kāi)詳情,也可以判斷網(wǎng)絡(luò)狀態(tài),wifi場(chǎng)景下可以將詳情數(shù)據(jù)都返回。
{ "md5"... // 校驗(yàn)所有item的detail,只有在新訂單,或訂單完成后移除的情況下,md5才會(huì)變化 "orderList":[{ "id"... "status"... "detail":{ // detail中盡量只保留變化情況較少的字段,避免md5頻繁變化,如status就移出到item中存放 "type"... "desc"... } },{ "id"... "status"... "detail":{ "type"... "desc"... } }] }總結(jié)暫時(shí)先這么多吧,水平有限,權(quán)當(dāng)是拋磚引玉,如果有更好的設(shè)計(jì),請(qǐng)?jiān)谖恼孪路搅粞愿嬖V我,交流想法,互相學(xué)習(xí)。謝謝支持~
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注