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