Talk is cheap. Show me your code

封装 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;

搞定~

posted @ 2021-07-20 15:38  Wise.Wrong  阅读(658)  评论(0编辑  收藏  举报