封装企业级别的fetch请求库

1,627 阅读3分钟

fetch的基本使用

/*
  向服务器发送数据请求的方案:
    第一类: XMLHttpRequest
      + ajax
      + axios 第三方库,对XMLHttpRequest进行封装(基于promise进行封装)
    第二类: fetch
      ES6内置的API,本身就是基于promise,用全新的方案实现客户端和服务器端的数据请求
      + 不兼容IE
      + 机制不够XMLHttpRequest完善(无法设置超时时间,没有内置的请求中断处理)
    第三类:其他方案,主要是跨域为主
      + jsonp
      + postMessage
      + 利用img的src发送请求,实现数据埋点和上报
      + ....

  let promise实例(p) = fetch(请求地址, 配置项);
    + 和axios的区别
      + 在fetch中,只要服务器有反馈信息(不管HTTP状态码是多少), 都说明网络请求成功, 最后的实例p都是fulfilled, 只有服务器没有任何反馈(例如:请求中断、请求超时、断网等), 实例p才是rejected!
      + 在axios中,只有返回的状态码以2开头,才会让实例是成功态!
      + fetch没有对GET请求,问号传参信息做特殊的处理(axios中基于params设置问号参数信息),fetch需要自己手动拼接到url末尾
    + 所以进入then的第一个回调,不一定说明请求成功,需要自行判断
    + 配置项
      + method 请求的方式, 默认是GET
      + cache 缓存模式 <>
      + credentials 资源凭证(例如cookie) <include, *same-origin, omit>
        fetch默认下,跨域请求中, 不允许携带资源凭证, 只有同源下才允许
        include: 同源和跨域都行
        omit: 都不行
      + headers: 普通对象{}/Headers实例
        自定义请求头信息
      + body 设置请求主体信息
        + 只适用于POST系列请求, 在GET系列请求中设置body会报错(让返回的实力变为失败态)
        + body内容格式有要求, 需要在请求头中指定 Content-Type 值
          + application/json JSON字符串
          + application/x-www-form-urlencoded
            'xxx=xxx&xxx=xxx'
          + multipart/form-data
            FormData对象,主要运用在文件上传(或者表单提交)的操作中
            let fm = new FormData();
            fm.append('file', 文件);
            // fm就是FormData对象
          + text/plain 普通字符串
          + ....

    + Headers头处理类, 请求头或者响应头
      Headers.prototype
        + append 新增头信息
        + delete 删除头信息
        + forEach((val, key) => {})
        + get 获取某一项的信息
        + has 验证是否包含某一项
        + ....
*/
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('name', 'zhufeng');
let p = fetch('/api/getTaskList?state=2', {
  /* headers: {
    'Content-Type': 'application/json'
  } */
  headers,
});
p.then(res => {
  /*
    res是服务器返回的response对象(Response类的实例)
      私有属性:
        + body 响应主体信息 是一个 ReadableStream 可读流
        + headers 响应头信息 Headers类的实例
        + status/statusText 返回的HTTP状态码和描述
      Response.prototype
        + arrayBuffer
        + blob
        + formData
        + json
        + text
        + ....
        这些方法是用来处理body可读流信息的, 把响应主体信息可读流信息转换为我们自己需要的格式!!
        这些方法的返回值是promise实例,可以避免服务器返回的信息在转换中出现问题(例如:服务器返回的是一个流信息,我们转化为json对象是不对的,此时可以让其返回失败的实例即可)
  */
  // {type: 'basic', url: 'http://localhost:3000/api/getTaskList', redirected: false, status: 200, ok: true, …}
  // {type: 'basic', url: 'http://localhost:3000/api/getTaskList1', redirected: false, status: 404, ok: false, …}
  const { headers, status, statusText } = res;
  console.log(headers.get('Date'));
  if(/^(2|3)\d{2}$/.test(status)) { // 代表成功
    // console.log('成功->', res.json()); // 这个方法不能重复调用
    return res.json();
  }
  // 获取数据失败
  return Promise.reject({
    code: -100,
    status,
    statusText
  });
  // throw new Error('获取数据失败了!');
}).then(res => {
  console.log('最终结果->', res);
}).catch(err => {
  // 失败的原因
  // 1. 服务器没有返回任何信息
  // 2. 状态码不对
  // 3. 数据转换失败
  console.log('失败->', err);
})

封装fetch请求库

/*
  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 { isPlainObject } from '../assets/utils'; // isPlainObject判断对象是不是一个纯对象
import qs from 'qs';
import { message } from 'antd';
const baseURL = '/api';
const http = function http(config) {
  if (!isPlainObject(config)) {
    config = {};
  }
  /* 设定默认值 */
  config = Object.assign({
    url: '',
    method: 'GET',
    credentials: 'include',
    reponseType: 'json',
    headers: null,
    body: null,
    params: null,
    signal: null
  }, config);
  /* 规则校验 */
  if (!config.url) {
    throw new TypeError('url is required!');
  }
  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
  }
  // 处理请求主体, 如果传递的是普通对象, 需要将其设置成urlencoded格式字符串, 如果是json格式字符串, 需要用 JSON.stringify() 进行转换
  if (isPlainObject(body)) {
    body = qs.stringify(body); // 已做 encodeURIComponent 处理
    headers['Content-Type'] = 'application/x-www-form-urlencoded';
    // headers['Content-Type'] = 'application/json';
  }
  // 统一处理,类似axios的请求拦截
  let token = localStorage.getItem('token');
  if (token) {
    headers['authorization'] = token;
  }
  // 整理配置项准备发送fetch请求
  method = method.toUpperCase();
  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:
              message.error('请求参数出现问题!');
              break;
            case 401: // 未登录 token过期 无权限
              // 未登录则跳转登录页面,并携带当前页面的路径
              // 在登录成功后返回当前页面,这一步需要在登录页操作。
              message.error('未授权,请重新登录!');
              localStorage.removeItem('token');
              break;
            case 403:
              message.error('服务器拒绝访问!');
              break;
            case 404:
              message.error('网络请求不存在!');
              break;
            default:
              message.error(`出错了!错误原因是 ${reason.status}: ${reason.statusText}`);
          }
        } else if(code === 20) { // 请求被中断
          message.error('请求被中断了~');
        } else {
          message.error('当前网络繁忙,请您稍后再试试吧~');
        }
      } else {
        message.error('当前网络繁忙,请您稍后再试试吧~');
      }
      // http.get('...').catch(() => {...})
      return Promise.reject(reason);
    })
};
// 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;