在前端頁面開發中,大部分的時間都是在與後端進行資料互動:獲取資料、計算並渲染。而頁面上又有大量的元素狀態需要維護,顯示、隱藏、變化。這些都可能讓我們焦頭爛額,然後在一週後看不懂自己的程式碼。
所以專案開發的過程中需要一個規範來約束程式碼的走向,讓程式碼能按照統一的、最高效的方式執行(還有讓別人閱讀)。這裡介紹一個前端對接後端介面資料的一個最佳實踐。
先讓我們看看反例,不知道你是不是用過這樣的 app:
- 點了一個按鈕沒有反應(???這按鈕壞了),但是突然頁面像爆炸了一樣不停的重新整理(-,-啊救命)
- 進入一個頁面,是個純白的(???網路卡了?程式報錯了?),返回再進還是純白,讓你搞不清楚到底發生了什麼。
這裡的例子說明:如果前端開發中不能把異常描述清楚、涵蓋全面,資料狀態的糟糕反饋就會直接影響使用者體驗。
問題分析
我們先從最簡單的情況入手,一個頁面使用一個介面。這種情況下通常是:
- 全量獲取列表
- 獲取主頁詳情
- 釋出一張圖片
- 搜尋關鍵詞
- ···
這樣的情況又分兩種,
- 進入頁面時獲取資料 -> 渲染頁面
- 進入頁面後進行操作 -> 得到反饋 -> 渲染頁面。
不論是哪一種情況,我們都只在一個頁面裡處理一個介面,這是最簡單的情況。那麼我們來看一下下面的圖片,並把它當作一個開發任務思考一下你會怎麼處理。
如果你只想到了**「呼叫介面」「渲染頁面」裡,那你這篇文章就是為你寫的(笑)。其實上面的圖只向你展示了兩個狀態**:「初始狀態」、「理想結果狀態」,我用了「理想結果」這個詞來描述這個狀態,是因為這是我們在一切操作都完美的情況下得到的理想狀態。
而通常在專案裡你只會從別人手裡得到這兩張圖,我說的對嗎?(產品經理和設計師都預設你瞭解他們需要的一切)。
如果我們希望做一個優秀的前端,我們就需要立刻發現這裡還缺少了三張圖(三個狀態)(有些互動裡並不需要這麼多狀態,這裡只討論最全面的情況)
資料獲取中狀態
無資料狀態
資料異常狀態
需求分析
從呼叫一個介面到渲染頁面我們大致分為一下幾部
呼叫介面 -> 得到資料 -> 處理資料 -> 渲染
初始狀態
接下來我們來編寫一些程式碼,來對接介面並且管理資料和狀態。為了使程式碼更加聚合,用一個字面量物件 SeaerchInput
來維護狀態。然後我們模擬一個介面的呼叫。
// 以上面搜尋為例
// 建立頁面物件
let SearchInput = {}
// 模擬一個介面
function API () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
let result = [{
name: `李三`
}];
resolve(result);
})
})
}
複製程式碼
理想結果狀態
接下來我們在SearchInput
中用data
欄位儲存資料,用getSearchResult()
方法繫結資料,呼叫介面並直接繫結資料,那麼我們將得到的「理想結果狀態」。
let SearchInput = {
data: null,
getSearchResult() {
API.then(
(res) => {
this.data = res; // 繫結資料
}
)
}
}
SearchInput.getSearchResult(); // 獲取資料
function API () {
return new Promise(function (resolve, reject) {
// ...
})
}
複製程式碼
// html 的語法將使用 angular 指令去表達
<div>
<!-- 渲染結果 -->
<p ng-repeat="result in SearchInput.data"></p>
</div>
複製程式碼
這樣的程式碼是十分脆弱的,因為我們已經預設資料會瞬間返回並且沒有任何問題。
資料獲取中狀態
function API () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
let result = [{
name: `李三`
}];
resolve(result);
}, 3000) // 為介面增加3秒的延時
})
}
複製程式碼
一旦給API
增加點延時,就會發現頁面會在純白狀態下停留很久,因為頁面沒有任何提示,所以使用者根本無法知道發生了什麼事情,是等待還是返回?
為此我們需要管理從介面發起請求(request)到接收響應(response)這段時間的狀態,在SearchInput
中用hasDone
來儲存介面的響應狀態,null
代表這個介面還在初始化狀態,false
代表已經發出請求但未收到響應,true
代表已經收到響應。
let SearchInput = {
data: null,
hasDone: null, // 初始化
getSearchResult() {
this.hasDone = false; // 發起請求時置為 false
API.then(
(res) => {
this.hasDone = true; // 收到響應時置為 true
this.data = res;
}
)
}
}
SearchInput.getSearchResult();
function API () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
// ...
}, 3000) // 為介面增加3秒的延時
})
}
複製程式碼
<!-- 資料獲取中狀態 -->
<div ng-if="SearchInput.hasDone === false">
loading
</div>
<div ng-if="SearchInput.hasDone">
<!-- 渲染結果 -->
<p ng-repeat="result in SearchInput.data"></p>
</div>
複製程式碼
這下好了,如果介面很慢頁面也會顯示 loading,使用者不會為此不知所措了。
資料異常狀態
儘管現在網路和伺服器已經十分穩定,很少會出現異常,但是無論是網路、伺服器或程式碼哪一個出現異常而沒有考慮,那都會造成用不好的使用者體驗
function API () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
let error = `伺服器異常`;
reject(error); // 介面返回了異常
})
})
}
複製程式碼
現在我們假設我們的API
返回了異常,頁面又會變為純白了,沒有任何資料顯示也沒有任何提示。
為此我們需要一個狀態來管理介面返回的狀態,在SearchInput
中用hasSuccess
來儲存介面的返回狀態,null
代表還在初始化狀態,false
代表介面返回失敗,true
代表介面成功返回資料。(你甚至可以先判斷資料的格式、數量等是否滿足你的要求,如果不滿足要求,即使介面返回了資料,你一樣可以將hasSuccess
設定為false
,因為這裡的 success 代表了你得到了可以正確使用的資料,而不僅僅是得到了資料)
let SearchInput = {
data: null,
hasDone: null,
hasSuccess: null, // 初始化
getSearchResult() {
this.hasDone = false;
API.then(
(res) => {
this.hasDone = true;
this.hasSuccess = true; // 得到資料置為 true
this.data = res;
},
(err) => {
this.hasDone = true; // 此時我們也要更新 hasDone
this.hasSuccess = false; // 發生異常置為 false
}
)
}
}
SearchInput.getSearchResult();
function API () {
return new Promise(function (resolve, reject) {
// ...
})
}
複製程式碼
<!-- 資料獲取中狀態 -->
<div ng-if="SearchInput.hasDone === false">
loading
</div>
<!-- 資料異常狀態 -->
<div ng-if="SearchInput.hasDone && SearchInput.hasSuccess === false">
資料異常
</div>
<div ng-if="SearchInput.hasDone && SearchInput.hasSuccess">
<!-- 渲染結果 -->
<p ng-repeat="result in SearchInput.data"></p>
</div>
複製程式碼
現在我們會在hasDone === true
後知道資料是否正常,並且給出了錯誤的提示。
無資料狀態
最後一個狀態也是我們要考慮的,當使用者嘗試搜尋一個詞卻什麼都沒返回,又變成了可惡的純白介面,我們還需要考慮一下當獲取資料時什麼都沒有的情況。
function API () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
let result = []; // 現在沒有任何結果
resolve(result);
})
})
}
複製程式碼
我們需要一個狀態來管理資料的狀態,在SearchInput
中用hasData
來儲存資料狀態,null
代表還在初始化中,false
代表資料為空,true
代表資料不為空。
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,沒有資料置為 false
this.data = res;
},
(err) => {
this.hasDone = true;
this.hasSuccess = false;
this.hasData = false; // 失敗肯定沒有資料了
}
)
}
}
SearchInput.getSearchResult();
function API () {
return new Promise(function (resolve, reject) {
// ...
})
}
複製程式碼
<!-- 資料獲取中狀態 -->
<div ng-if="SearchInput.hasDone === false">
loading
</div>
<!-- 資料異常狀態 -->
<div ng-if="SearchInput.hasDone && SearchInput.hasSuccess === false">
資料異常
</div>
<!-- 無資料狀態 -->
<div ng-if="SearchInput.hasDone && SearchInput.hasSuccess && SearchInput.hasData === false">
資料異常
</div>
<div ng-if="SearchInput.hasDone && SearchInput.hasSuccess && SearchInput.hasData">
<!-- 渲染結果 -->
<p ng-repeat="result in SearchInput.data"></p>
</div>
複製程式碼
現在上面的程式碼基本上就是你所需要的了,它可以幫你應對各種情況,讓頁面展示的更加完美。
實踐分析
這一大段程式碼就是對應一個簡單介面五個狀態的設計,也是我目前專案中使用的模式,雖然看上去比較繁瑣,但是相比後期再不停的補充和修改,一次性考慮全面帶來很多好處。
如果一個介面是為了實現分頁載入,那麼狀態的數量又會有所提升,這篇文章不再闡述。
如果一個頁面使用了多個介面,資料和狀態之間產生了交叉,為了使狀態邏輯清晰應該合理利用字面量物件來聚合程式碼邏輯。
在多人協作方面,由於大家使用同一套規範,對程式碼的閱讀速度有顯著提高。
這裡列出的程式碼以普及為主,很多實現細節方面都可以再去優化,提煉。甚至寫一個建構函式也是很方便的選擇。
羅小黑寫寫文字
如果喜歡文章 請留下一個贊~
如果喜歡文章 分享給更多人~自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證)
轉載時請保留原文連結 以保證可及時獲取對文章的訂正和修改