Eaxios - 更容易處理響應的網絡請求庫

Eaxios 是基於 axios 封裝的網絡請求庫,在保持 API 與 axios 基本一致的狀況下,簡化服務端響應內容和各類異常狀況的處理。前端

開發背景

errors.svg

如上圖所示,是一次 Ajax 請求可能輸出的結果,在前端咱們須要根據輸出結果給用戶不一樣的提示。ios

  • 請求被取消:忽略
  • 網絡異常:提示檢查是否鏈接網絡
  • 請求超時:提示網絡慢,請切換網絡
  • 服務器異常:提示系統出問題了
  • 響應解析失敗:同上,且能夠進行錯誤日誌上報
  • 請求失敗:這種狀況一般是業務異常,前端須要根據錯誤碼進行相應的處理,最簡單的就是消息提醒
  • 請求成功:前端拿到數據後更新界面

可是,現有的 Axios 庫對於異常結果沒有提供較好的封裝,Axios Promise catch 裏包含各類類型的錯誤,並且沒有提供錯誤碼來識別請求失敗的緣由。並且不少服務端接口會返回本身的錯誤碼,這樣在 Axios Promise then 裏也須要處理業務異常。git

此外,Axios 自己以下所述的一些問題和侷限性。github

  • 若是設置 Axios responseType 爲 json 時,服務端返回的非 JSON 格式的響應內容會由於沒法解析,response.data 爲 null

    對於 500 等錯誤,響應內容會丟失,因此不要去配置 responseType 爲 json,對於使用者來講容易採到這個坑。web

    ps:雖然 Axios 官方文檔聲明 responseType 是 json,實際上底層調用 XMLHttpRequest 的 responseType 是沒有傳值的,應該是爲了規避這個問題。npm

  • Axios 默認無論 HTTP 響應狀態和 responseType 是什麼,都會調用默認的 transformResponse

    ps:應該是爲了規避上一個問題,默認提供了一個響應處理函數進行 JSON 解析,可是這會影響性能(500 等響應內容值較多時,會形成頁面卡頓)。雖然 transformResponse 能夠轉換 response,實際接收到的參數是 response.data,因此沒法判斷具體狀況來決定是否進行解析 JSON。json

  • Axios then 和 catch 是根據 validateStatus 決定的,使用者處理以來較爲麻煩。

    理想狀況下,使用者但願 then 返回有效的數據,catch 返回各類錯誤狀況:請求被取消、網絡異常、網絡超時、服務端異常、服務端數據格式錯誤、業務異常。axios

  • Axios 默認不處理 content-typeapplication/x-www-form-urlencoded 類型的請求體,使用起來不夠方便

優化方案:api

  • 若是設置 Axios responseType 爲 json 時,不要傳給傳 XMLHttpRequest,以免非 JSON 格式的響應內容丟失
  • Axios 根據響應頭的 content-type 判斷是否須要解析 JSON,以免性能問題

    經過請求攔截器實現不給 Axios 傳遞 transformResponse 配置,且將配置備份到其餘字段上,而後在響應攔截器中將響應對象 response 傳遞給 transformResponse 處理。響應攔截器根據 response 提供的狀態碼、響應頭和響應內容判斷是否要進行 JSON 轉換。服務器

  • 取消 Axios validateStatus 的配置選項,默認全部大於 0 的狀態碼都是正確的狀態碼,而後在 Axios 攔截器 then 中進行數據解析(非 200 的可能也是 JSON,因此要複用 200 的 JSON 解析代碼),而且根據異常狀況拋出直觀的錯誤對象
  • 內置默認處理表單類型的請求體

用法說明

eaxios 主要對響應的處理作了一些優化,除了如下部分,eaxios 的 api 與 axios 保持一致:

  • eaxios 請求配置的 transformResponse 傳參和處理時機發生了變化

    axios 在服務端響應內容後就會調用 transformResponse 進行響應轉換,eaxios 響應後內部自動根據響應頭和 responseType 進行 JSON 解析,而後將解析後的數據和 response 傳給 transformResponse,transformResponse 返回的數據最終會被 Promise resovle 給外部調用者。

    假設服務端返回的數據結構爲 { code: 0, message: 'success', data: {} },code 爲 0 表示正確的響應,非 0 表示異常,接口請求的代碼示例以下所示:

    const eaxios = require('eaxios');
    
    eaxios.defaults.transformResponse = [
      function (data, response) {
        if (typeof data === 'object') {
          // 默認約定有成功解析 JSON 對象,就認爲服務端成功響應,且有提供錯誤碼
          if (data.code === 0) {
            return data.data;
          } else {
            throw eaxios.createError(data.message, data.code, response);
          }
        } else {
          // 50x 等服務異常狀況
          throw eaxios.createError(
            data,
            response.config.responseError.SERVER_ERROR,
            response
          );
        }
      },
    ];
    
    return eaxios('https://run.mocky.io/v3/4f503449-0349-467e-a38a-c804956712b7')
      .then((data) => {
        console.log('success', data.id);
      })
      .catch((error) => {
        console.log('failure', error.code); // UNKNOWN、REQUEST_OFFLINE、REQUEST_TIMEOUT、SERVER_ERROR、RESPONSE_INVALID 和業務錯誤碼
      });

    ps:若是存在服務單接口請求規範,能夠經過 eaxios.create 建立適用於不一樣接口規範的請求函數。

  • eaxios 的請求處理函數 then 只會接收到 transformResponse 轉換後的數據,對於網絡、超時、服務端異常和業務異常等問題,會在 catch 接收一個 EaxiosError 類型的錯誤對象。

    interface EaxiosError<T = any> extends Error {
      config: EaxiosRequestConfig;
      code?: string;
      request?: any;
      response?: EaxiosResponse<T>;
      isAxiosError: boolean;
      toJSON: () => object;
    }

    錯誤處理函數能夠根據錯誤碼 code 來處理異常,code 可能的值爲 UNKNOWN、REQUEST_OFFLINE、REQUEST_TIMEOUT、SERVER_ERROR、RESPONSE_INVALID 和其餘業務錯誤碼。

    ps:若是要定製錯誤碼,能夠在請求配置中添加配置項 responseError

    eaxios.defaults.responseError = {
      REQUEST_OFFLINE: '1'REQUEST_OFFLINE
    };
  • eaxios 內部會自動序列化表單類型的請求參數,因此只要對象給 data 就好了。

代碼示例

下面以 { code: 0, message: 'success', data: { } } 這樣的接口規範爲例,演示如何使用 eaxios。

const eaxios = require('eaxios');

const request = eaxios.create({
  baseURL: 'https://run.mocky.io/v3',
  timeout: 30000,
  transformResponse: [
    function (data, response) {
      if (typeof data === 'object') {
        if (data.code === 0) {
          return data.data;
        } else {
          throw eaxios.createError(data.message, data.code, response);
        }
      } else {
        throw eaxios.createError(
          data,
          response.config.responseError.SERVER_ERROR,
          response,
        );
      }
    },
  ],
});

request.interceptors.response.use(
  function (response) {
    return response;
  },
  function (error) {
    if (error && error.code) {
      if (error.code === 'UNKNOWN') {
        console.log('未知錯誤');
      } else if (error.code === 'REQUEST_OFFLINE') {
        console.log('網絡未鏈接');
      } else if (error.code === 'REQUEST_TIMEOUT') {
        console.log('網絡有點慢,請求超時了');
      } else if (error.code === 'SERVER_ERROR') {
        console.log('系統出問題了');
      } else if (error.code === 'RESPONSE_INVALID') {
        console.log('服務端 bug');
      } else if (error.code === '10000') {
        // 假設 10000 爲登陸會話過時
        console.log('登陸會話失效');
      } else {
        console.log('根據狀況是否要消息提示,仍是外部處理')
      }
    }
    throw error;
  },
);

function printError(error) {
  console.log(
    `code: ${error.code}, name: ${error.name}, message: ${error.message}, isAxiosError: ${error.isAxiosError}, stack:\n${error.stack}`,
  );
}

function success() {
  console.log('>> success');
  return request('/4f503449-0349-467e-a38a-c804956712b7')
    .then((data) => {
      console.log('success', data);
    })
    .catch((error) => {
      printError(error);
    });
}

function failure() {
  console.log('>> failure');
  return request('/42d7c21d-5ae6-4b52-9c2d-4c3dd221eba4')
    .then((data) => {
      console.log('success', data);
    })
    .catch((error) => {
      printError(error);
    });
}

function invalid() {
  console.log('>> invalid');
  return request('/1b23549f-c918-4362-9ac8-35bc275c09f0')
    .then((data) => {
      console.log('success', data);
    })
    .catch((error) => {
      printError(error);
    });
}

function server_500() {
  console.log('>> server_500');
  return request('/2a9d8c00-9688-4d36-b2de-2dee5e81f5b3')
    .then((data) => {
      console.log('success', data);
    })
    .catch((error) => {
      printError(error);
    });
}

success().then(failure).then(invalid).then(server_500);
/* log
>> success
success { id: 1 }
>> failure
登陸會話失效
code: 10000, name: Error, message: error, isAxiosError: true, stack: ...
>> invalid
服務端 bug
code: RESPONSE_INVALID, name: SyntaxError, message: Unexpected token V in JSON at position 0, isAxiosError: true, stack: ...
>> server_500
系統出問題了
code: SERVER_ERROR, name: Error, message: ...,  stack: ...
*/

兼容性

eaxios 依賴 URLSearchParams 處理表單類型的請求參數,不支持的環境須要引入響應的 polyfill

相關連接

相關文章
相關標籤/搜索