調用 ajax 取請求後端數據是項目中最基礎的功能。但是如果每次直接調用底層的瀏覽器 api 去發請求則非常麻煩。現在來分析一下怎麼封裝這一層,看看有哪些基礎問題需要考慮。本文底層使用 fetch ,如果你使用 XMLHttpRequest 甚至第三方庫(譬如:axios)封裝過程都是大同小異的。
封裝重複代碼
對於同一個項目通常來說請求參數有很多重複的內容,譬如 url 的拼接,http head 的設置。假設我們調用的是 RESTful 接口,通常我們需要變動的有:1. 請求 url 的 path 部分;2. 參數;3. 請求 method;4. 成功/失敗回調函數。我們看下把重複代碼封裝成一個 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,
// 這裏假設我們項目請求頭固定這兩個
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 ( /* 判斷返回沒有錯誤 */ ) {
success && success( resultJson );
} else {
fail && fail( resultJson.error );
}
} );
}
}
使調用可讀性更好
以上封裝了一個 ApiSender,調用的時候如下:
ApiSender.send( '/resource', 'GET', {
pageSize: 10,
pageNo: 1
}, function( result ){
// 對結果進行處理
}, function( error ){
alert( error )
} )
通過傳遞迴調函數的方式,可讀性性不是很好(當然這是一個仁者見仁的問題)。我們把返回改成 Promise。因爲我們用的是 fetch,它直接返回的就是 Promise,比較好改。如果你底層用的是 XMLHttpRequest,那麼可以自行把調用 XMLHttpRequest 的代碼封裝在一個 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,
// 這裏假設我們項目請求頭固定這兩個
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()
} );
}
}
調用的時候代碼就變成:
ApiSender.send( '/resource', 'GET', {pageSize:10,pageNo:1} ).then( function(result){
if ( /* 判斷返回沒有錯誤 */ ) {
// 處理結果
} else {
// 提示錯誤
}
} )
從調用者角度抽象返回值
上面代碼有一個問題,對於 ApiSend 的調用者來說,他需要直接處理接口返回值,判斷是否成功。如果接口返回對象比較簡單還好,如果非常複雜,那麼調用者就很頭疼,舉個例子,我碰到過如下的接口返回值:
{
content: {
result: {
errorCode: 1,
errorMessage: '',
isSuccess: true
},
data: {}|[] // 真正的可用數據
},
a: { // 有特徵的字段名我做了簡化,使用了a,ab這樣的字段名。a 這個字段內容是 api 網關層包裝的。
code: 1,
ab: [ {
code: 1
} ]
}
}
如何判斷這個返回值是成功的呢?
let result = { /* 上面那個對象 */ }
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
) {
// 處理結果 result.content.data
}
}
你想象下,作爲 ApiSender 的調用方,會希望得到什麼結果?執行正確的時候獲得接口返回的數據,執行異常的時候獲得錯誤信息。我不希望調用一個方法,需要通過複雜地解析返回值來判斷是否成功。所以最直觀的就是把錯誤封裝成一個很直觀的返回值:
let ApiSender = {
send( options ) {
/* 代碼省略掉了 */
return fetch( /* 參數也省略掉了 */ ).then( function(response){
let result = response.json();
if ( isSuccessResult(result) ) {
return [ null, result.content.data ]
} else {
let error = parseError( result );
return [ error, null ];
}
} );
}
}
那麼調用方對結果的判斷就非常方便了:
ApiSender.send( '/resource', 'GET', {pageSize:10,pageNo:1} ).then( function([error,data]){
if ( !error ) {
// 處理結果 data
} else {
alert( error ); // error 的格式大家可以自行定義,各個項目各有不同
}
} );
面向切面需要做些什麼
以上一個比較基礎且簡潔的封裝就做好了,但是現實中有些基礎功能是經常需要的,譬如請求日誌,請求錯誤報錯統一處理。如果這些代碼需要調用方來做,一來代碼重複,二來譬如日誌應該是調用方不感知的一個功能。所以我們對代碼進一步進行優化,加入這些功能:
let ApiSender = {
send( options ) {
/* 代碼省略掉了 */
return fetch( /* 參數也省略掉了 */ ).then( function(response){
let result = response.json();
// 記錄調用日誌
writeLog( options, result );
if ( isSuccessResult(result) ) {
return [ null, result.content.data ]
} else {
let error = parseError( result );
// 界面報錯
MessageComponent.error( `${error.message}(${error.code})` );
return [ error, null ];
}
} );
}
}
日誌你可以上傳服務器,也可以就本地 console,日誌記錄哪些內容,參數如何都按各自的項目需求而定。如此的話,調用方就更簡潔了:
ApiSender.send( '/resource', 'GET', {pageSize:10,pageNo:1} ).then( function([error,data]){
if ( !error ) {
// 處理結果 data
}
} );
絕大多數情況下,調用接口返回錯誤是需要在頁面上提示錯誤的,但是並不是所有情況都需要。譬如非用戶觸發的行爲,且請求返回的結果並不嚴重影響頁面操作或者流程。那麼我們可以在調用 ApiSender 的時候加一個參數,允許調用方跳過全局錯誤處理:
let ApiSender = {
send( options ) {
/* 代碼省略掉了 */
let skipErrorHandler = options.skipErrorHandler;
return fetch( /* 參數也省略掉了 */ ).then( function(response){
let result = response.json();
// 記錄調用日誌
writeLog( options, result );
if ( isSuccessResult(result) ) {
return [ null, result.content.data ]
} else {
let error = parseError( result );
// 傳了這個參數才跳過,不傳或者傳了非 true 值(當然包括 false),都認爲不跳過
if ( skipErrorHandler===true ) {
// 界面報錯
MessageComponent.error( `${error.message}(${error.code})` );
}
return [ error, null ];
}
} );
}
}
所以如果你希望自己處理錯誤,調用的時候代碼就是:
ApiSender.send( '/resource', 'GET', {skipErrorHandler:true/*, 其他參數 */} ).then( function([error,data]){
if ( !error ) {
// 處理結果 data
} else {
// 自行處理錯誤
}
} );
到這裏爲止,請求層的基本封裝算是比較完整了,不過最後有一個小點要考慮下,如果你在 fetch().then 傳入的回調函數中因爲種種原因而拋出了異常(譬如某個字段沒有判空)。那麼 ApiSender 的調用方是沒法感知的,程序直接就報錯了。所以爲了程序的健壯性,我們最後再加一個 catch:
let ApiSender = {
send( options ) {
/* 代碼省略掉了 */
let skipErrorHandler = options.skipErrorHandler;
return fetch( /* 參數也省略掉了 */ ).then( function(response){
let result = response.json();
// 記錄調用日誌
writeLog( options, result );
if ( isSuccessResult(result) ) {
return [ null, result.content.data ]
} else {
let error = parseError( result );
// 傳了這個參數才跳過,不傳或者傳了非 true 值(當然包括 false),都認爲不跳過
if ( skipErrorHandler===true ) {
// 界面報錯
MessageComponent.error( `${error.message}(${error.code})` );
}
return [ error, null ];
}
} ).catch( function(error){
return [ error, null ];
} );
}
}
這樣一個對調用方友好,避免代碼重複的請求層就封裝好了。PS: 如果對 Promise 的 api 不是很熟悉的話,可以先了解下,有助於更好的理解示例代碼。