1. 程式人生 > 其它 >封裝 fetch 與 Error,返回 Promise 物件

封裝 fetch 與 Error,返回 Promise 物件

fetch在目前已經是很成熟的請求資源的方法,但為了方便在專案中呼叫,一般都會進行二次封裝

一、定義錯誤型別

對於封裝公共元件或方法,一定要多想,七分設計,三分開發

而對於一個網路請求來說,除了處理請求體、響應體之外,還有一個常常被忽略的環節,那就是定義 Error

專案中關於網路請求的錯誤有很多種,比如超時錯誤、伺服器錯誤、斷網錯誤,這些都可以加以封裝

// errors.js

export class ApiError extends Error {
  constructor(message, url) {
    super(message);
    this.message = message;
    
this.name = 'ApiError'; this.url = url; } } export class DisconnectError extends ApiError { constructor(url) { super('網路已斷開,請重新連線', url); this.name = 'DisconnectError'; } } export class ApiServerError extends ApiError { constructor(statusCode, url) { super(`請求伺服器出錯:${statusCode}`, url);
this.name = 'ApiServerError'; this.statusCode = statusCode; } } export class ApiJsonError extends ApiError { constructor(url) { super('請求伺服器出錯:無法轉換為JSON', url); this.name = 'ApiJsonError'; } } export class ApiTimeoutError extends ApiError { constructor(time, url) { super('請求超時
', url); this.name = 'ApiTimeoutError'; this.time = time; } }

上面定義了幾種常見的請求錯誤,其實還可以定義一種業務錯誤

比如某個請求的狀態是 200,但不符合後端定義的業務邏輯,返回了特殊的 code

這時就可以根據後端返回的 code 進行業務錯誤的封裝

二、丟擲錯誤

在丟擲上面定義的 Error 的時候,需要做一些判斷,這部分邏輯可以抽出來

// 檢查網路狀態是否已連線
function checkonLine(url) {
  return new Promise((resolve, reject) => {
    if (!window.navigator.onLine) {
      reject(new DisconnectError(url));
    } else {
      resolve(url);
    }
  });
}

// 校驗狀態碼
function checkStatus(response) {
  const status = Number(response.status);
  if (status >= 200 && status < 300) {
    return response;
  }
  throw new ApiServerError(status, response.url);
}

// 解析 fetch 的響應結果
function parseJSON(response) {
  return new Promise((resolve, reject) => {
    response
      .json()
      .then((json) => {
        // 記錄請求的地址
        // eslint-disable-next-line
        json._SERVER_URL = response.url;
        resolve(json);
      })
      .catch((error) => {
        if (error instanceof SyntaxError) {
          reject(new ApiJsonError(response.url));
        } else {
          reject(error);
        }
      });
  });
}

三、封裝 fetch

準備就緒,可以上主菜了

const FETCH_TIMEOUT = 1000 * 30;

function request(path, params, options = {}) {
  const { body, method = 'GET', ...other } = params || {};
  const newMethod = `${method}`.toUpperCase();
  const newParams = { method: newMethod, credentials: 'include', ...other };
  const timeout = newParams.timeout || FETCH_TIMEOUT;

  let url = path;

  if (newMethod !== 'GET') {
    if (!(body instanceof FormData)) {
      newParams.headers = {
        Accept: 'application/json',
        'Content-Type': 'application/json; charset=utf-8',
        ...newParams.headers,
      };
      newParams.body = JSON.stringify(body);
    }
  } else {
    // 對GET請求增加時間戳 以避免IE快取
    const timestamp = Date.now();
    const queryURL = qs.stringify({ ...body, t: timestamp });
    url = `${url}?${queryURL}`;
  }

  // 封裝請求頭
  newParams.headers = {
    ...newParams.headers,
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest',
    credentials: 'include',
  };
  return Promise.race([
    // 校驗網路連線
    checkonLine(url)
      .then(() => {
        return fetch(url, newParams, options);
      })
      .then(checkStatus)
      .then(parseJSON)
      .catch((err) => {
        throw err;
      }),
    new Promise((resolve, reject) => {
      setTimeout(reject, timeout, new ApiTimeoutError(timeout, url));
    }),
  ]);
}

請求函式 request 已經搞定,這時可以簡單的粗暴的 export 這個函式

也可以匯出具體的 get、post 方法

export default {
  get: async (path, params, options) =>
    request(path, { method: 'GET', body: params }, options),
  post: async (path, params, options) =>
    request(path, { method: 'POST', body: params }, options),
  delete: async (path, params, options) =>
    request(path, { method: 'DELETE', body: params }, options),
  put: async (path, params, options) =>
    request(path, { method: 'PUT', body: params }, options),
};

四、更進一步

上面的程式碼匯出的是一個含有 get 等方法的物件,需要這麼使用:

import http from './request'

http.get('/api/get', { name: 'wise' });
http.post('/api/save', { name: 'wise' });

不過對於 get 請求,很多的庫做了進一步的封裝,可以直接呼叫

// 直接呼叫,預設使用 get 請求
http('/api/get', { name: 'wise' });

為了更好的體驗,我們的程式碼也可以更進一步:

const http = (url) => {
  return http.get(url);
};

http.get = async (path, params, options) =>
  request(path, { method: 'GET', body: params }, options);
http.post = async (path, params, options) =>
  request(path, { method: 'POST', body: params }, options);
http.delete = async (path, params, options) =>
  request(path, { method: 'DELETE', body: params }, options);
http.put = async (path, params, options) =>
  request(path, { method: 'PUT', body: params }, options);

export default http;

搞定~