在前端頁(yè)面開發(fā)中镶摘,大部分的時(shí)間都是在與后端進(jìn)行數(shù)據(jù)交互:獲取數(shù)據(jù)嗽桩、計(jì)算并渲染。而頁(yè)面上又有大量的元素狀態(tài)需要維護(hù)凄敢,顯示碌冶、隱藏、變化涝缝。這些都可能讓我們焦頭爛額扑庞,然后在一周后看不懂自己的代碼譬重。
所以項(xiàng)目開發(fā)的過程中需要一個(gè)規(guī)范來約束代碼的走向,讓代碼能按照統(tǒng)一的罐氨、最高效的方式運(yùn)行(還有讓別人閱讀)害幅。這里介紹一個(gè)前端對(duì)接后端接口數(shù)據(jù)的一個(gè)最佳實(shí)踐。
先讓我們看看反例岂昭,不知道你是不是用過這樣的 app:
- 點(diǎn)了一個(gè)按鈕沒有反應(yīng)(以现??约啊?這按鈕壞了)邑遏,但是突然頁(yè)面像爆炸了一樣不停的刷新(-,-啊救命)
- 進(jìn)入一個(gè)頁(yè)面恰矩,是個(gè)純白的(记盒??外傅?網(wǎng)卡了纪吮?程序報(bào)錯(cuò)了?)萎胰,返回再進(jìn)還是純白碾盟,讓你搞不清楚到底發(fā)生了什么。
這里的例子說明:如果前端開發(fā)中不能把異常描述清楚技竟、涵蓋全面冰肴,數(shù)據(jù)狀態(tài)的糟糕反饋就會(huì)直接影響用戶體驗(yàn)。
問題分析
我們先從最簡(jiǎn)單的情況入手榔组,一個(gè)頁(yè)面使用一個(gè)接口熙尉。這種情況下通常是:
- 全量獲取列表
- 獲取主頁(yè)詳情
- 發(fā)布一張圖片
- 搜索關(guān)鍵詞
- ···
這樣的情況又分兩種,
- 進(jìn)入頁(yè)面時(shí)獲取數(shù)據(jù) -> 渲染頁(yè)面
- 進(jìn)入頁(yè)面后進(jìn)行操作 -> 得到反饋 -> 渲染頁(yè)面搓扯。
不論是哪一種情況检痰,我們都只在一個(gè)頁(yè)面里處理一個(gè)接口,這是最簡(jiǎn)單的情況锨推。那么我們來看一下下面的圖片铅歼,并把它當(dāng)作一個(gè)開發(fā)任務(wù)思考一下你會(huì)怎么處理。
如果你只想到了「調(diào)用接口」「渲染頁(yè)面」里爱态,那你這篇文章就是為你寫的(笑)谭贪。其實(shí)上面的圖只向你展示了兩個(gè)狀態(tài):「初始狀態(tài)」、「理想結(jié)果狀態(tài)」锦担,我用了「理想結(jié)果」這個(gè)詞來描述這個(gè)狀態(tài)俭识,是因?yàn)檫@是我們?cè)谝磺胁僮鞫纪昝赖那闆r下得到的理想狀態(tài)。
而通常在項(xiàng)目里你只會(huì)從別人手里得到這兩張圖洞渔,我說的對(duì)嗎套媚?(產(chǎn)品經(jīng)理和設(shè)計(jì)師都默認(rèn)你了解他們需要的一切)缚态。
如果我們希望做一個(gè)優(yōu)秀的前端,我們就需要立刻發(fā)現(xiàn)這里還缺少了三張圖(三個(gè)狀態(tài))(有些交互里并不需要這么多狀態(tài)堤瘤,這里只討論最全面的情況)
數(shù)據(jù)獲取中狀態(tài)
無數(shù)據(jù)狀態(tài)
數(shù)據(jù)異常狀態(tài)
需求分析
從調(diào)用一個(gè)接口到渲染頁(yè)面我們大致分為一下幾部
調(diào)用接口 -> 得到數(shù)據(jù) -> 處理數(shù)據(jù) -> 渲染
初始狀態(tài)
接下來我們來編寫一些代碼玫芦,來對(duì)接接口并且管理數(shù)據(jù)和狀態(tài)。為了使代碼更加聚合本辐,用一個(gè)字面量對(duì)象 SeaerchInput
來維護(hù)狀態(tài)桥帆。然后我們模擬一個(gè)接口的調(diào)用。
// 以上面搜索為例
// 創(chuàng)建頁(yè)面對(duì)象
let SearchInput = {}
// 模擬一個(gè)接口
function API () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
let result = [{
name: '李三'
}];
resolve(result);
})
})
}
理想結(jié)果狀態(tài)
接下來我們?cè)?code>SearchInput中用data
字段保存數(shù)據(jù)慎皱,用getSearchResult()
方法綁定數(shù)據(jù)老虫,調(diào)用接口并直接綁定數(shù)據(jù),那么我們將得到的「理想結(jié)果狀態(tài)」茫多。
let SearchInput = {
data: null,
getSearchResult() {
API.then(
(res) => {
this.data = res; // 綁定數(shù)據(jù)
}
)
}
}
SearchInput.getSearchResult(); // 獲取數(shù)據(jù)
function API () {
return new Promise(function (resolve, reject) {
// ...
})
}
// html 的語法將使用 angular 指令去表達(dá)
<div>
<!-- 渲染結(jié)果 -->
<p ng-repeat="result in SearchInput.data"></p>
</div>
這樣的代碼是十分脆弱的祈匙,因?yàn)槲覀円呀?jīng)默認(rèn)數(shù)據(jù)會(huì)瞬間返回并且沒有任何問題。
數(shù)據(jù)獲取中狀態(tài)
function API () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
let result = [{
name: '李三'
}];
resolve(result);
}, 3000) // 為接口增加3秒的延時(shí)
})
}
一旦給API
增加點(diǎn)延時(shí)天揖,就會(huì)發(fā)現(xiàn)頁(yè)面會(huì)在純白狀態(tài)下停留很久夺欲,因?yàn)轫?yè)面沒有任何提示,所以用戶根本無法知道發(fā)生了什么事情今膊,是等待還是返回些阅?
為此我們需要管理從接口發(fā)起請(qǐng)求(request)到接收響應(yīng)(response)這段時(shí)間的狀態(tài),在SearchInput
中用hasDone
來保存接口的響應(yīng)狀態(tài)万细,null
代表這個(gè)接口還在初始化狀態(tài)扑眉,false
代表已經(jīng)發(fā)出請(qǐng)求但未收到響應(yīng),true
代表已經(jīng)收到響應(yīng)赖钞。
let SearchInput = {
data: null,
hasDone: null, // 初始化
getSearchResult() {
this.hasDone = false; // 發(fā)起請(qǐng)求時(shí)置為 false
API.then(
(res) => {
this.hasDone = true; // 收到響應(yīng)時(shí)置為 true
this.data = res;
}
)
}
}
SearchInput.getSearchResult();
function API () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
// ...
}, 3000) // 為接口增加3秒的延時(shí)
})
}
<!-- 數(shù)據(jù)獲取中狀態(tài) -->
<div ng-if="SearchInput.hasDone === false">
loading
</div>
<div ng-if="SearchInput.hasDone">
<!-- 渲染結(jié)果 -->
<p ng-repeat="result in SearchInput.data"></p>
</div>
這下好了,如果接口很慢頁(yè)面也會(huì)顯示 loading聘裁,用戶不會(huì)為此不知所措了雪营。
數(shù)據(jù)異常狀態(tài)
盡管現(xiàn)在網(wǎng)絡(luò)和服務(wù)器已經(jīng)十分穩(wěn)定,很少會(huì)出現(xiàn)異常衡便,但是無論是網(wǎng)絡(luò)献起、服務(wù)器或代碼哪一個(gè)出現(xiàn)異常而沒有考慮,那都會(huì)造成用不好的用戶體驗(yàn)
function API () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
let error = '服務(wù)器異常';
reject(error); // 接口返回了異常
})
})
}
現(xiàn)在我們假設(shè)我們的API
返回了異常镣陕,頁(yè)面又會(huì)變?yōu)榧儼琢饲床停瑳]有任何數(shù)據(jù)顯示也沒有任何提示。
為此我們需要一個(gè)狀態(tài)來管理接口返回的狀態(tài)呆抑,在SearchInput
中用hasSuccess
來保存接口的返回狀態(tài)岂嗓,null
代表還在初始化狀態(tài),false
代表接口返回失敗鹊碍,true
代表接口成功返回?cái)?shù)據(jù)厌殉。(你甚至可以先判斷數(shù)據(jù)的格式食绿、數(shù)量等是否滿足你的要求,如果不滿足要求公罕,即使接口返回了數(shù)據(jù)器紧,你一樣可以將hasSuccess
設(shè)置為false
,因?yàn)檫@里的 success 代表了你得到了可以正確使用的數(shù)據(jù)楼眷,而不僅僅是得到了數(shù)據(jù))
let SearchInput = {
data: null,
hasDone: null,
hasSuccess: null, // 初始化
getSearchResult() {
this.hasDone = false;
API.then(
(res) => {
this.hasDone = true;
this.hasSuccess = true; // 得到數(shù)據(jù)置為 true
this.data = res;
},
(err) => {
this.hasDone = true; // 此時(shí)我們也要更新 hasDone
this.hasSuccess = false; // 發(fā)生異常置為 false
}
)
}
}
SearchInput.getSearchResult();
function API () {
return new Promise(function (resolve, reject) {
// ...
})
}
<!-- 數(shù)據(jù)獲取中狀態(tài) -->
<div ng-if="SearchInput.hasDone === false">
loading
</div>
<!-- 數(shù)據(jù)異常狀態(tài) -->
<div ng-if="SearchInput.hasDone && SearchInput.hasSuccess === false">
數(shù)據(jù)異常
</div>
<div ng-if="SearchInput.hasDone && SearchInput.hasSuccess">
<!-- 渲染結(jié)果 -->
<p ng-repeat="result in SearchInput.data"></p>
</div>
現(xiàn)在我們會(huì)在hasDone === true
后知道數(shù)據(jù)是否正常铲汪,并且給出了錯(cuò)誤的提示。
無數(shù)據(jù)狀態(tài)
最后一個(gè)狀態(tài)也是我們要考慮的罐柳,當(dāng)用戶嘗試搜索一個(gè)詞卻什么都沒返回桥状,又變成了可惡的純白界面,我們還需要考慮一下當(dāng)獲取數(shù)據(jù)時(shí)什么都沒有的情況硝清。
function API () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
let result = []; // 現(xiàn)在沒有任何結(jié)果
resolve(result);
})
})
}
我們需要一個(gè)狀態(tài)來管理數(shù)據(jù)的狀態(tài)辅斟,在SearchInput
中用hasData
來保存數(shù)據(jù)狀態(tài),null
代表還在初始化中芦拿,false
代表數(shù)據(jù)為空士飒,true
代表數(shù)據(jù)不為空。
let SearchInput = {
data: null,
hasDone: null,
hasSuccess: null,
hasData: null, // 初始化
getSearchResult() {
this.hasDone = false;
API.then(
(res) => {
this.hasDone = true;
this.hasSuccess = true;
this.hasData = res.length > 0; // 有置為 true蔗崎,沒有數(shù)據(jù)置為 false
this.data = res;
},
(err) => {
this.hasDone = true;
this.hasSuccess = false;
this.hasData = false; // 失敗肯定沒有數(shù)據(jù)了
}
)
}
}
SearchInput.getSearchResult();
function API () {
return new Promise(function (resolve, reject) {
// ...
})
}
<!-- 數(shù)據(jù)獲取中狀態(tài) -->
<div ng-if="SearchInput.hasDone === false">
loading
</div>
<!-- 數(shù)據(jù)異常狀態(tài) -->
<div ng-if="SearchInput.hasDone && SearchInput.hasSuccess === false">
數(shù)據(jù)異常
</div>
<!-- 無數(shù)據(jù)狀態(tài) -->
<div ng-if="SearchInput.hasDone && SearchInput.hasSuccess && SearchInput.hasData === false">
數(shù)據(jù)異常
</div>
<div ng-if="SearchInput.hasDone && SearchInput.hasSuccess && SearchInput.hasData">
<!-- 渲染結(jié)果 -->
<p ng-repeat="result in SearchInput.data"></p>
</div>
現(xiàn)在上面的代碼基本上就是你所需要的了酵幕,它可以幫你應(yīng)對(duì)各種情況,讓頁(yè)面展示的更加完美缓苛。
實(shí)踐分析
這一大段代碼就是對(duì)應(yīng)一個(gè)簡(jiǎn)單接口五個(gè)狀態(tài)的設(shè)計(jì)芳撒,也是我目前項(xiàng)目中使用的模式,雖然看上去比較繁瑣未桥,但是相比后期再不停的補(bǔ)充和修改笔刹,一次性考慮全面帶來很多好處。
如果一個(gè)接口是為了實(shí)現(xiàn)分頁(yè)加載冬耿,那么狀態(tài)的數(shù)量又會(huì)有所提升舌菜,這篇文章不再闡述。
如果一個(gè)頁(yè)面使用了多個(gè)接口亦镶,數(shù)據(jù)和狀態(tài)之間產(chǎn)生了交叉日月,為了使?fàn)顟B(tài)邏輯清晰應(yīng)該合理利用字面量對(duì)象來聚合代碼邏輯。
在多人協(xié)作方面,由于大家使用同一套規(guī)范,對(duì)代碼的閱讀速度有顯著提高评腺。
這里列出的代碼以普及為主希痴,很多實(shí)現(xiàn)細(xì)節(jié)方面都可以再去優(yōu)化,提煉。甚至寫一個(gè)構(gòu)造函數(shù)也是很方便的選擇蜕琴。
羅小黑寫寫文字
如果喜歡文章 請(qǐng)留下一個(gè)贊~
如果喜歡文章 分享給更多人~在掘金中關(guān)注我
在簡(jiǎn)書中關(guān)注我自由轉(zhuǎn)載-非商用-非衍生-保持署名(創(chuàng)意共享3.0許可證)
轉(zhuǎn)載時(shí)請(qǐng)保留原文鏈接 以保證可及時(shí)獲取對(duì)文章的訂正和修改