Fetch封装

javascript
/*
  http([config]);
    + url 请求地址
    + method
    + credentials 携带资源凭证 *include, same-origin, omit
    + headers: null 自定义请求头信息 必须是纯粹对象
    + body: null 请求主体信息 只针对与POST系列请求 根据当前服务器要求 如果用户传递的是一个纯粹对象 需要转为urlencoded格式字符串
    + params: null 设定问号传参信息 格式必须是纯粹对象 在内部我们将其拼接在url的末尾
    + reponseType 预设服务器返回结果的读取方式 *json/text/arrayBuffer/blob
    + signal 中断请求的信号
  ----------
  http.get/head/delete/options([url], [config])  预先指定了预置项中的url/method
  http.post/put/patch([url], [body], [config])  预先指定了预置项中的url/method/body
  ----------
  e.g.
    http.get('/api/xxx', {...});
    http({
      method: 'GET',
      url: '/api/xxx',
      ...
    });

    http.post('/api/xxx', {}, {...});
    http({
      method: 'POST',
      url: '/api/xxx',
      body: {},
      ...
    });
*/
import { getToken } from '@/utils/auth';
import qs from 'qs';

const baseURL = '/api';
const pendingRequests = {};

// 防抖API白名单
const ABORT_WHITE_LIST = [];

function isObject(o) {
  return Object.prototype.toString.call(o) === '[object Object]';
}

// 判断对象是不是一个纯对象
function isPlainObject(o) {
  var ctor, prot;

  if (isObject(o) === false) { return false; }

  ctor = o.constructor;
  if (ctor === undefined) { return true; }

  prot = ctor.prototype;
  if (isObject(prot) === false) { return false; }

  if (prot.hasOwnProperty('isPrototypeOf') === false) {
    return false;
  }

  return true;
}

const http = function http(config) {
  // 规则校验
  if (!config.url) {
    throw new TypeError('url is required!');
  }

  let controller = null

  // 防抖处理
  if (!ABORT_WHITE_LIST.includes(config.url)) {

    const previousController = pendingRequests[config.url];

    // 存在相同URL则中止请求
    if (previousController) {
      previousController.abort();
    }

    if (!isPlainObject(config)) {
      config = {};
    }

    controller = new AbortController();
    pendingRequests[config.url] = controller
  }

  // 设定默认值
  config = Object.assign({
    url: '',
    method: 'GET',
    credentials: 'include',
    reponseType: 'json',
    headers: null,
    body: null,
    params: null,
    signal: controller ? controller.signal : null
  }, config);

  if (!isPlainObject(config.headers)) {
    config.headers = {};
  }
  if (config.params !== null && !isPlainObject(config.params)) {
    config.params = null;
  }

  // 开始处理
  let { url, method, credentials, headers, body, params, signal, reponseType } = config;

  // 处理问号传参
  url = baseURL + url;
  if (params) {
    url += `${url.indexOf('?') === -1 ? '?' : '&'}${qs.stringify(params)}`; // xxx=xxx&xxx=xxx
  }

  // 处理请求主体
  method = method.toUpperCase();

  params = qs.stringify(params);

  if (method === 'GET') {
    headers['Content-Type'] = 'application/x-www-form-urlencoded'
  } else if (['HEAD', 'DELETE', 'OPTIONS'].includes(method) === 'POST') {
    // TODO

  } else {
    body = JSON.stringify(body); // 已做 encodeURIComponent 处理
    headers['Content-Type'] = 'application/json;charset=UTF-8';
  }

  // 统一处理,类似axios的请求拦截
  let token = getToken();
  if (token) {
    headers['Authorization'] = token;
  }

  // 整理配置项准备发送fetch请求
  config = {
    method,
    credentials,
    headers,
    signal,
    cache: 'no-cache'
  }
  if (/^(POST|PUT|PATCH)$/.test(method) && body) {
    config.body = body;
  }

  return fetch(url, config)
    .then(response => { // 响应拦截
      let { status, statusText } = response;
      if (/^(2|3)\d{2}$/.test(status)) { // 请求成功
        let res;
        // *json/text/arrayBuffer/blob
        switch (reponseType) { // 这些函数执行也可能因为流转换失败,返回失败promise实例
          case 'text':
            res = response.text();
            break;
          case 'arrayBuffer':
            res = response.arrayBuffer();
            break;
          case 'blob':
            res = response.blob();
            break;
          default:
            res = response.json();
        }
        return res;
      }
      // 请求失败 HTTP状态码失败
      return Promise.reject({
        code: -100,
        status,
        statusText
      });
    })
    .catch(reason => { // 失败的统一提示
      if (reason && typeof reason === 'object') {
        let { code, status } = reason;
        if (code === -100) { // 状态码出错
          switch (+status) {
            case 400:
              console.error('请求参数出现问题!');
              break;
            case 401: // 未登录 token过期 无权限
              // 未登录则跳转登录页面,并携带当前页面的路径
              // 在登录成功后返回当前页面,这一步需要在登录页操作。
              console.error('未授权,请重新登录!');
              localStorage.removeItem('token');
              break;
            case 403:
              console.error('服务器拒绝访问!');
              break;
            case 404:
              console.error('网络请求不存在!');
              break;
            default:
              console.error(`出错了!错误原因是 ${reason.status}: ${reason.statusText}`);
          }
        } else if (code === 20) { // 请求被中断
          console.error('请求被中断了~');
        } else {
          console.error('当前网络繁忙,请您稍后再试试吧~');
        }
      } else {
        console.error('当前网络繁忙,请您稍后再试试吧~');
      }
      // http.get('...').catch(() => {...})
      return Promise.reject(reason);
    })
    .finally(() => {
      delete pendingRequests[config.url]
    })
};

// get系列的快捷方法
['GET', 'HEAD', 'DELETE', 'OPTIONS'].forEach(method => {
  http[method.toLowerCase()] = function (url, config) {
    if (!isPlainObject(config)) {
      config = {};
    }
    config['url'] = url;
    config['method'] = method;
    return http(config);
  }
});

// post系列的快捷方法
['POST', 'PUT', 'PATCH'].forEach(method => {
  http[method.toLowerCase()] = function (url, body, config) {
    if (!isPlainObject(config)) {
      config = {};
    }
    config['url'] = url;
    config['body'] = body;
    config['method'] = method;
    return http(config);
  }
});

export default http;