前端項目請求層封裝過程

調用 ajax 取請求後端數據是項目中最基礎的功能。可是若是每次直接調用底層的瀏覽器 api 去發請求則很是麻煩。如今來分析一下怎麼封裝這一層,看看有哪些基礎問題須要考慮。本文底層使用 fetch ,若是你使用 XMLHttpRequest 甚至第三方庫(譬如:axios)封裝過程都是大同小異的。javascript

封裝重複代碼

對於同一個項目一般來講請求參數有不少重複的內容,譬如 url 的拼接,http head 的設置。假設咱們調用的是 RESTful 接口,一般咱們須要變更的有:1. 請求 url 的 path 部分;2. 參數;3. 請求 method;4. 成功/失敗回調函數。咱們看下把重複代碼封裝成一個 ApiSender 的示例代碼:java

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,調用的時候以下:ios

ApiSender.send( '/resource', 'GET', {
  pageSize: 10,
  pageNo: 1
}, function( result ){
  // 對結果進行處理
}, function( error ){
  alert( error )
} )

經過傳遞迴調函數的方式,可讀性性不是很好(固然這是一個仁者見仁的問題)。咱們把返回改爲 Promise。由於咱們用的是 fetch,它直接返回的就是 Promise,比較好改。若是你底層用的是 XMLHttpRequest,那麼能夠自行把調用 XMLHttpRequest 的代碼封裝在一個 Promise 中返回。ajax

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()
    } );
  }
}

調用的時候代碼就變成:json

ApiSender.send( '/resource', 'GET', {pageSize:10,pageNo:1} ).then( function(result){
  if ( /* 判斷返回沒有錯誤 */ ) {
    // 處理結果
  } else {
    // 提示錯誤
  }
} )

從調用者角度抽象返回值

上面代碼有一個問題,對於 ApiSend 的調用者來講,他須要直接處理接口返回值,判斷是否成功。若是接口返回對象比較簡單還好,若是很是複雜,那麼調用者就很頭疼,舉個例子,我碰到過以下的接口返回值:axios

{
  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 的調用方,會但願獲得什麼結果?執行正確的時候得到接口返回的數據,執行異常的時候得到錯誤信息。我不但願調用一個方法,須要經過複雜地解析返回值來判斷是否成功。因此最直觀的就是把錯誤封裝成一個很直觀的返回值:api

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 不是很熟悉的話,能夠先了解下,有助於更好的理解示例代碼。

相關文章
相關標籤/搜索