調(diào)用 ajax 取請(qǐng)求后端數(shù)據(jù)是項(xiàng)目中最基礎(chǔ)的功能。但是如果每次直接調(diào)用底層的瀏覽器 api 去發(fā)請(qǐng)求則非常麻煩『桑現(xiàn)在來分析一下怎么封裝這一層宠纯,看看有哪些基礎(chǔ)問題需要考慮。本文底層使用 fetch 层释,如果你使用 XMLHttpRequest 甚至第三方庫(譬如:axios)封裝過程都是大同小異的婆瓜。
封裝重復(fù)代碼
對(duì)于同一個(gè)項(xiàng)目通常來說請(qǐng)求參數(shù)有很多重復(fù)的內(nèi)容,譬如 url 的拼接,http head 的設(shè)置廉白。假設(shè)我們調(diào)用的是 RESTful 接口个初,通常我們需要變動(dòng)的有:1. 請(qǐng)求 url 的 path 部分;2. 參數(shù)猴蹂;3. 請(qǐng)求 method院溺;4. 成功/失敗回調(diào)函數(shù)。我們看下把重復(fù)代碼封裝成一個(gè) ApiSender 的示例代碼:
const URL_PREFIX = 'xxx';
let ApiSender = {
send( options ) {
let {
path,
params,
method,
success,
fail
} = options;
let url = URL_PREFIX + path;
if ( method==='GET' ) {
url += ('?'+toQueryString( params ));
}
let requestBody;
if ( method==='POST' ) {
requestBody = params;
}
fetch( url, {
method: method,
// 這里假設(shè)我們項(xiàng)目請(qǐng)求頭固定這兩個(gè)
headers: {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
credentials: 'include',
body: requestBody
} ).then( function(response){
let resultJson = response.json();
if ( /* 判斷返回沒有錯(cuò)誤 */ ) {
success && success( resultJson );
} else {
fail && fail( resultJson.error );
}
} );
}
}
使調(diào)用可讀性更好
以上封裝了一個(gè) ApiSender磅轻,調(diào)用的時(shí)候如下:
ApiSender.send( '/resource', 'GET', {
pageSize: 10,
pageNo: 1
}, function( result ){
// 對(duì)結(jié)果進(jìn)行處理
}, function( error ){
alert( error )
} )
通過傳遞回調(diào)函數(shù)的方式珍逸,可讀性性不是很好(當(dāng)然這是一個(gè)仁者見仁的問題)。我們把返回改成 Promise聋溜。因?yàn)槲覀冇玫氖?fetch谆膳,它直接返回的就是 Promise,比較好改撮躁。如果你底層用的是 XMLHttpRequest漱病,那么可以自行把調(diào)用 XMLHttpRequest 的代碼封裝在一個(gè) Promise 中返回。
let ApiSender = {
send( options ) {
let {
path,
params,
method,
success,
fail
} = options;
let url = URL_PREFIX + path;
if ( method==='GET' ) {
url += toQueryString( params );
}
let requestBody;
if ( method==='POST' ) {
requestBody = params;
}
return fetch( url, {
method: method,
// 這里假設(shè)我們項(xiàng)目請(qǐng)求頭固定這兩個(gè)
headers: {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
credentials: 'include',
body: requestBody
} ).then( function(response){
return response.json()
} );
}
}
調(diào)用的時(shí)候代碼就變成:
ApiSender.send( '/resource', 'GET', {pageSize:10,pageNo:1} ).then( function(result){
if ( /* 判斷返回沒有錯(cuò)誤 */ ) {
// 處理結(jié)果
} else {
// 提示錯(cuò)誤
}
} )
從調(diào)用者角度抽象返回值
上面代碼有一個(gè)問題把曼,對(duì)于 ApiSend 的調(diào)用者來說杨帽,他需要直接處理接口返回值,判斷是否成功祝迂。如果接口返回對(duì)象比較簡(jiǎn)單還好睦尽,如果非常復(fù)雜,那么調(diào)用者就很頭疼型雳,舉個(gè)例子,我碰到過如下的接口返回值:
{
content: {
result: {
errorCode: 1,
errorMessage: '',
isSuccess: true
},
data: {}|[] // 真正的可用數(shù)據(jù)
},
a: { // 有特征的字段名我做了簡(jiǎn)化山害,使用了a纠俭,ab這樣的字段名。a 這個(gè)字段內(nèi)容是 api 網(wǎng)關(guān)層包裝的浪慌。
code: 1,
ab: [ {
code: 1
} ]
}
}
如何判斷這個(gè)返回值是成功的呢冤荆?
let result = { /* 上面那個(gè)對(duì)象 */ }
if (
result.a &&
result.a.code === 0 &&
result.a.ab &&
result.a.ab[ 0 ] &&
result.a.ab[ 0 ].code === 0
) {
if (
result.content &&
result.content.result &&
result.content.result.isSuccess === true
) {
// 處理結(jié)果 result.content.data
}
}
你想象下,作為 ApiSender 的調(diào)用方权纤,會(huì)希望得到什么結(jié)果钓简?執(zhí)行正確的時(shí)候獲得接口返回的數(shù)據(jù),執(zhí)行異常的時(shí)候獲得錯(cuò)誤信息汹想。我不希望調(diào)用一個(gè)方法外邓,需要通過復(fù)雜地解析返回值來判斷是否成功。所以最直觀的就是把錯(cuò)誤封裝成一個(gè)很直觀的返回值:
let ApiSender = {
send( options ) {
/* 代碼省略掉了 */
return fetch( /* 參數(shù)也省略掉了 */ ).then( function(response){
let result = response.json();
if ( isSuccessResult(result) ) {
return [ null, result.content.data ]
} else {
let error = parseError( result );
return [ error, null ];
}
} );
}
}
那么調(diào)用方對(duì)結(jié)果的判斷就非常方便了:
ApiSender.send( '/resource', 'GET', {pageSize:10,pageNo:1} ).then( function([error,data]){
if ( !error ) {
// 處理結(jié)果 data
} else {
alert( error ); // error 的格式大家可以自行定義古掏,各個(gè)項(xiàng)目各有不同
}
} );
面向切面需要做些什么
以上一個(gè)比較基礎(chǔ)且簡(jiǎn)潔的封裝就做好了损话,但是現(xiàn)實(shí)中有些基礎(chǔ)功能是經(jīng)常需要的,譬如請(qǐng)求日志,請(qǐng)求錯(cuò)誤報(bào)錯(cuò)統(tǒng)一處理丧枪。如果這些代碼需要調(diào)用方來做光涂,一來代碼重復(fù),二來譬如日志應(yīng)該是調(diào)用方不感知的一個(gè)功能拧烦。所以我們對(duì)代碼進(jìn)一步進(jìn)行優(yōu)化忘闻,加入這些功能:
let ApiSender = {
send( options ) {
/* 代碼省略掉了 */
return fetch( /* 參數(shù)也省略掉了 */ ).then( function(response){
let result = response.json();
// 記錄調(diào)用日志
writeLog( options, result );
if ( isSuccessResult(result) ) {
return [ null, result.content.data ]
} else {
let error = parseError( result );
// 界面報(bào)錯(cuò)
MessageComponent.error( `${error.message}(${error.code})` );
return [ error, null ];
}
} );
}
}
日志你可以上傳服務(wù)器,也可以就本地 console恋博,日志記錄哪些內(nèi)容齐佳,參數(shù)如何都按各自的項(xiàng)目需求而定。如此的話交播,調(diào)用方就更簡(jiǎn)潔了:
ApiSender.send( '/resource', 'GET', {pageSize:10,pageNo:1} ).then( function([error,data]){
if ( !error ) {
// 處理結(jié)果 data
}
} );
絕大多數(shù)情況下重虑,調(diào)用接口返回錯(cuò)誤是需要在頁面上提示錯(cuò)誤的,但是并不是所有情況都需要秦士。譬如非用戶觸發(fā)的行為缺厉,且請(qǐng)求返回的結(jié)果并不嚴(yán)重影響頁面操作或者流程。那么我們可以在調(diào)用 ApiSender 的時(shí)候加一個(gè)參數(shù)隧土,允許調(diào)用方跳過全局錯(cuò)誤處理:
let ApiSender = {
send( options ) {
/* 代碼省略掉了 */
let skipErrorHandler = options.skipErrorHandler;
return fetch( /* 參數(shù)也省略掉了 */ ).then( function(response){
let result = response.json();
// 記錄調(diào)用日志
writeLog( options, result );
if ( isSuccessResult(result) ) {
return [ null, result.content.data ]
} else {
let error = parseError( result );
// 傳了這個(gè)參數(shù)才跳過提针,不傳或者傳了非 true 值(當(dāng)然包括 false),都認(rèn)為不跳過
if ( skipErrorHandler===true ) {
// 界面報(bào)錯(cuò)
MessageComponent.error( `${error.message}(${error.code})` );
}
return [ error, null ];
}
} );
}
}
所以如果你希望自己處理錯(cuò)誤曹傀,調(diào)用的時(shí)候代碼就是:
ApiSender.send( '/resource', 'GET', {skipErrorHandler:true/*, 其他參數(shù) */} ).then( function([error,data]){
if ( !error ) {
// 處理結(jié)果 data
} else {
// 自行處理錯(cuò)誤
}
} );
到這里為止辐脖,請(qǐng)求層的基本封裝算是比較完整了,不過最后有一個(gè)小點(diǎn)要考慮下皆愉,如果你在 fetch().then 傳入的回調(diào)函數(shù)中因?yàn)榉N種原因而拋出了異常(譬如某個(gè)字段沒有判空)嗜价。那么 ApiSender 的調(diào)用方是沒法感知的,程序直接就報(bào)錯(cuò)了幕庐。所以為了程序的健壯性久锥,我們最后再加一個(gè) catch:
let ApiSender = {
send( options ) {
/* 代碼省略掉了 */
let skipErrorHandler = options.skipErrorHandler;
return fetch( /* 參數(shù)也省略掉了 */ ).then( function(response){
let result = response.json();
// 記錄調(diào)用日志
writeLog( options, result );
if ( isSuccessResult(result) ) {
return [ null, result.content.data ]
} else {
let error = parseError( result );
// 傳了這個(gè)參數(shù)才跳過,不傳或者傳了非 true 值(當(dāng)然包括 false)异剥,都認(rèn)為不跳過
if ( skipErrorHandler===true ) {
// 界面報(bào)錯(cuò)
MessageComponent.error( `${error.message}(${error.code})` );
}
return [ error, null ];
}
} ).catch( function(error){
return [ error, null ];
} );
}
}
這樣一個(gè)對(duì)調(diào)用方友好瑟由,避免代碼重復(fù)的請(qǐng)求層就封裝好了。PS: 如果對(duì) Promise 的 api 不是很熟悉的話冤寿,可以先了解下歹苦,有助于更好的理解示例代碼。