Ajax 和異步處理
調(diào)用 API 訪問數(shù)據(jù)采用的 Ajax 方式闸盔,這是一個異步過程,異步過程最基本的處理方式是事件或回調(diào)琳省,其實這兩種處理方式實現(xiàn)原理差不多迎吵,都需要在調(diào)用異步過程的時候傳入一個在異步過程結(jié)束的時候調(diào)用的接口。比如 jQuery Ajax 的 success
就是典型的回調(diào)參數(shù)针贬。不過使用 jQuery 處理異步推薦使用 Promise 處理方式击费。
Promise 處理方式也是通過注冊回調(diào)函數(shù)來完成的。jQuery 的 Promise 和 ES6 的標(biāo)準(zhǔn) Promise 有點不一樣桦他,但在 then
上可以兼容蔫巩,通常稱為 thenable。jQuery 的 Promise 沒有提供 .catch()
接口快压,但它自己定義的 .done()
圆仔、.fail()
和 .always()
三個注冊回調(diào)的方式也很有特色,用起來很方便蔫劣,它是在事件的方式來注冊的(即坪郭,可以注冊多個同類型的處理函數(shù),在該觸發(fā)的時候都會觸發(fā))拦宣。
當(dāng)然更直觀的一點的處理方式是使用 ES2017 帶來的 async/await 方式截粗,可以用同步代碼的形式來寫異步代碼,當(dāng)然也有一些坑在里面鸵隧。對于前端工程師來說绸罗,最大的坑就是有些瀏覽器不支持,需要進行轉(zhuǎn)譯豆瘫,所以如果前端代碼沒有構(gòu)建過程珊蟀,一般還是就用 ES5 的語法兼容性好一些(jQuery 的 Promise 是支持 ES5 的,但是標(biāo)準(zhǔn) Promise 要 ES6 以后才可以使用)外驱。
關(guān)于 JavaScript 異步處理相關(guān)的內(nèi)容可以參考
- 從小小題目逐步走進 JavaScript 異步調(diào)用
- 閑談異步調(diào)用“扁平”化
- 從地獄到天堂育灸,Node 回調(diào)向 async/await 轉(zhuǎn)變
- 理解 JavaScript 的 async/await
- 從不用 try-catch 實現(xiàn)的 async/await 語法說錯誤處理
自己封裝工具函數(shù)
在處理 Ajax 的過程中,雖然有現(xiàn)成的庫(比如 jQuery.ajax昵宇,axios 等)磅崭,它畢竟是為了通用目的設(shè)計的,在使用的時候仍然不免繁瑣瓦哎。而在項目中砸喻,對 Api 進行調(diào)用的過程幾乎都大同小異柔逼。如果設(shè)計得當(dāng),就連錯誤處理的方式都會是一樣的割岛。因此愉适,在項目內(nèi)的 Ajax 調(diào)用其實可以進行進一步的封裝,使之在項目內(nèi)使用起來更方便癣漆。如果接口方式發(fā)生變化维咸,修改起來也更容易。
比如惠爽,當(dāng)前接口要求使用 POST 方法調(diào)用(暫不考慮 RESTful)癌蓖,參數(shù)必須包括 action
,返回的數(shù)據(jù)以 JSON 方式提供疆股,如果出錯费坊,只要不是服務(wù)器異常都會返回特定的 JSON 數(shù)據(jù),包括一個不等于 0 的 code
和可選的 message
屬性旬痹。
那么用 jQuery 寫這么一個 Ajax 調(diào)用附井,大概是這樣
const apiUrl = "http://api.some.com/";
jQuery
.ajax(url, {
type: "post",
dataType: "json",
data: {
action: "login",
username: "uname",
password: "passwd"
}
})
.done(function(data) {
if (data.code) {
alert(data.message || "登錄失敗两残!");
} else {
window.location.assign("home");
}
})
.fail(function() {
alert("服務(wù)器錯誤");
});
初步封裝
同一項目中永毅,這樣的 Ajax 調(diào)用,基本上只有 data
部分和 .done
回調(diào)中的 else
部分不同人弓,所以進行一次封裝會大大減少代碼量沼死,可以這樣封裝
function appAjax(action, params) {
var deffered = $.Deferred();
jQuery
.ajax(apiUrl, {
type: "post",
dataType: "json",
data: $.extend({
action: action
}, params)
})
.done(function(data) {
// 當(dāng) code 為 0 或省略時,表示沒有錯誤崔赌,
// 其它值表示錯誤代碼
if (data.code) {
if (data.message) {
// 如果服務(wù)器返回了消息意蛀,那么向用戶呈現(xiàn)消息
// resolve(null),表示不需要后續(xù)進行業(yè)務(wù)處理
alert(data.message);
deffered.resolve();
} else {
// 如果服務(wù)器沒返回消息健芭,那么把 data 丟給外面的業(yè)務(wù)處理
deferred.reject(data);
}
} else {
// 正常返回數(shù)據(jù)的情況
deffered.resolve(data);
}
})
.fail(function() {
// Ajax 調(diào)用失敗县钥,向用戶呈現(xiàn)消息,同時不需要進行后續(xù)的業(yè)務(wù)處理
alert("服務(wù)器錯誤");
deffered.resolve();
});
return deferred.promise();
}
而業(yè)務(wù)層的調(diào)用就很簡單了
appAjax("login", {
username: "uname",
password: "passwd"
}).done(function(data) {
if (data) {
window.location.assign("home");
}
}).fail(function() {
alert("登錄失敗");
});
更換 API 調(diào)用接口
上面的封裝對調(diào)用接口和返回數(shù)據(jù)進行了統(tǒng)一處理慈迈,把大部分項目接口約定的內(nèi)容都處理掉了若贮,剩下在每次調(diào)用時需要處理的就是純粹的業(yè)務(wù)。
現(xiàn)在項目組決定不用 jQuery 的 Ajax痒留,而是采用 axios 來調(diào)用 API(axios 不見得就比 jQuery 好谴麦,這里只是舉例),那么只需要修改一下 appAjax()
的實現(xiàn)即可伸头。所有業(yè)務(wù)調(diào)用都不需要修改匾效。
假設(shè)現(xiàn)在的目標(biāo)環(huán)境仍然是 ES5,那么需要第三方 Promise 提供恤磷,這里擬用 Bluebird面哼,兼容原生 Promise 接口(在 HTML 中引入雪侥,未直接出現(xiàn)在 JS 代碼中)。
function appAjax(action, params) {
var deffered = $.Deferred();
axios
.post(apiUrl, {
data: $.extend({
action: action
}, params)
})
.then(function(data) { ... }, function() { ... });
return deferred.promise();
}
這次的封裝采用了 axios 來實現(xiàn) Web Api 調(diào)用精绎。但是為了保持原來的接口(jQuery Promise 對象有提供 .done()
、.fail()
和 .always()
事件處理)锌妻,appAjax
仍然不得不返回 jQuery Promise代乃。這樣,即使所有地方都不再需要使用 jQuery仿粹,這里仍然得用搁吓。
項目中應(yīng)該用還是不用 jQuery?請閱讀為什么要用原生 JavaScript 代替 jQuery吭历?
去除 jQuery
就只在這里使用 jQuery 總讓人感覺如芒在背堕仔,想把它去掉。有兩個辦法
- 修改所有業(yè)務(wù)中的調(diào)用晌区,去掉
.done()
摩骨、.fail()
和.always()
,改成.then()
朗若。這一步工作量較大恼五,但基本無痛,因為 jQuery Promise 本身支持.then()
哭懈。但是有一點需要特別注意灾馒,這一點稍后說明 - 自己寫個適配器,兼容 jQuery Promise 的接口遣总,工作量也不小睬罗,但關(guān)鍵是要充分測試,避免差錯旭斥。
上面提到第 1 種方法中有一點需要特別注意容达,那就是 .then()
和 .done()
系列函數(shù)在處理方式上有所不同。.then()
是按 Promise 的特性設(shè)計的琉预,它返回的是另一個 Promise 對象董饰;而 .done()
系列函數(shù)是按事件機制實現(xiàn)的,返回的是原來的 Promise 對象圆米。所以像下面這樣的代碼在修改時就要注意了
appAjax(url, params)
.done(function(data) { console.log("第 1 處處理", data) })
.done(function(data) { console.log("第 2 處處理", data) });
// 第 1 處處理 {}
// 第 2 處處理 {}
簡單的把 .done()
改成 .then()
之后(注意不需要使用 Bluebird卒暂,因為 jQuery Promise 支持 .then()
)
appAjax(url, params)
.then(function(data) { console.log("第 1 處處理", data); })
.then(function(data) { console.log("第 2 處處理", data); });
// 第 1 處處理 {}
// 第 2 處處理 undefined
原因上面已經(jīng)講了,這里正確的處理方式是合并多個 done 的代碼娄帖,或者在 .then()
處理函數(shù)中返回 data
:
appAjax(url, params)
.then(function(data) {
console.log("第 1 處處理", data);
return data;
})
.then(function(data) {
console.log("第 2 處處理", data);
});
使用 Promise 接口改善設(shè)計
我們的 appAjax()
接口部分也可以設(shè)計成 Promise 實現(xiàn)也祠,這是一個更通用的接口。既使用不用 ES2015+ 特性近速,也可以使用像 jQuery Promise 或 Bluebird 這樣的三方庫提供的 Promise诈嘿。
function appAjax(action, params) {
// axios 依賴于 Promise堪旧,ES5 中可以使用 Bluebird 提供的 Promise
return axios
.post(apiUrl, {
data: $.extend({
action: action
}, params)
})
.then(function(data) {
// 這里調(diào)整了判斷順序,會讓代碼看起來更簡潔
if (!data.code) { return data; }
if (!data.message) { throw data; }
alert(data.message);
}, function() {
alert("服務(wù)器錯誤");
});
}
不過現(xiàn)在前端有構(gòu)建工具奖亚,可以使用 ES2015+ 配置 Babel淳梦,也可以使用 TypeScript …… 總之,選擇很多昔字,寫起來也很方便爆袍。那么在設(shè)計的時候就不用局限于 ES5 所支持的內(nèi)容了。所以可以考慮用 Promise + async/await 來實現(xiàn)
async function appAjax(action, params) {
// axios 依賴于 Promise作郭,ES5 中可以使用 Bluebird 提供的 Promise
const data = await axios
.post(apiUrl, {
data: $.extend({
action: action
}, params)
})
// 這里模擬一個包含錯誤消息的結(jié)果陨囊,以便后面統(tǒng)一處理錯誤
// 這樣就不需要用 try ... catch 了
.catch(() => ({ code: -1, message: "服務(wù)器錯誤" }));
if (!data.code) { return data; }
if (!data.message) { throw data; }
alert(data.message);
}
上面代碼中使用
.catch()
來避免try ... catch ...
的技巧在從不用 try-catch 實現(xiàn)的 async/await 語法說錯誤處理中提到過。
當(dāng)然業(yè)務(wù)層調(diào)用也可以使用 async/await(記得寫在 async 函數(shù)中):
const data = await appAjax("login", {
username: "uname",
password: "passwd"
}).catch(() => {
alert("登錄失敗");
});
if (data) {
window.location.assign("home");
}
對于多次 .done()
的改造:
const data = await appAjax(url, params);
console.log("第 1 處處理", data);
console.log("第 2 處處理", data);
小結(jié)
本文以封裝 Ajax 調(diào)用為例夹攒,看似在講述異步調(diào)用蜘醋。但實際想告訴大家的東西是:如何將一個常用的功能封裝起來,實現(xiàn)代碼重用和更簡潔的調(diào)用咏尝;以及在封裝的過程中需要考慮的問題——向前和向后的兼容性压语,在做工具函數(shù)封裝的時候,應(yīng)該盡量避免和某個特定的工具特性綁定状土,向公共標(biāo)準(zhǔn)靠攏——不知大家是否有所體會无蜂。