Fetch及Fetch的二次封装

404 阅读5分钟

Fetch请求的介绍

developer.mozilla.org/zh-CN/docs/…

只要服务器有响应,不论HTTP状态码为多少,Fetch都会把promise实例设置为成功

response 是Response内置类的实例

  • status/statusText 状态码及其描述
  • headers 是Headers内置类的实例,基于Headers.prototype上的方法,可以获取响应头的信息
    • get([key])
    • has([key])
    • keys/values/entries 返回迭代器对象,基于next方法执行可以一次获取响应头的信息
    • forEach 循环迭代每一个返回的响应头信息
  • body 存储的是响应主体信息,它是一个ReadableStream可读流

Response.prototype

  • arrayBuffer 以Buffer格式数据读取
  • blob
  • json
  • text
  • ...

执行这几个方法,返回的结果是一个promise实例;原因:服务器返回的数据内容格式和我们要读取的方法可能存在误差,例如:服务器返回的是普通文本,而我们基于json方法去获取,想要获取json对象,这样是无法正常读取出来的,此时可以把promise标记为失败...而且这样读取的过程也可以是异步操作的,一旦本次执行了某个方法,则无法再执行其他的方法

服务器没有响应:断开请求 & 网络出现故障,Fetch才会把promise实例设置为失败

  • 基于AbortController断开请求 err={message: 'The user aborted a request.',code:20, name:'AbortError'}
  • 如果从服务器成功获取内容(状态码以2、3开始的),但是读取数据失败,也会进入这里(err是Error对象,具备message属性记录失败原因)
  • 如果从服务器获取的内容,但是状态码不符合要求,也会进入到这里(err是自定义的信息对象)

Fetch在当初设计的时候,并没有设置超时和断开,没有类似于XHR的监控上传下载进度

Fetch的兼容性也比较差IE都不支持,想要兼容 基于@bebal/polyfill 是不够的,还需要基于 fetch-polyfill 处理

  • 不考虑兼容的情况下
  • 目前实现fetch的中断处理,可以基于AbortController实现

XMLHTTPRequest相对完善的

/* 测试fetch应用 */
const controller = new AbortController(); // 创建一个控制器
fetch('/api/news_latest', {
  signal: controller.signal, // 传递控制器的信号
})
  .then((response) => {
    let { status, statusText } = response;
    if (status >= 200 && status < 400) {
      return response.json();
    }
    return Promise.reject({
      code: 'STATUS ERROR',
      status,
      statusText,
    });
  })
  .then((res) => {
    console.log('成功:', res);
  })
  .catch((err) => {
    console.log(err);
  });
controller.abort();

options配置

fetch('/api/user', {
  method: 'GET', // 设置请求方式, 默认是GET
  credentials: 'include', // 设置是否允许携带资源凭证 omit都不允许,same-origin只允许同源,include允许跨域
  headers: {
    'Content-Type': 'application/json',
  }, //
  // body:{}, // 只有在POST/PUT请求下才允许设置body(设置请求主体,但是需要再headers中指定对应的类型的Content-Type值(MIME类型))
  // signal: xxx, // 用于取消请求
  cache: 'no-cache', // 设置缓存模式
  mode: 'cors', // 设置请求模式
})
  .then((response) => {
    let { status, statusText, headers, url, ok } = response;
    if (status >= 200 && status < 300) {
      return response.json();
    }
    return Promise.reject(new Error(statusText));
  })
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.log(error);
  });

Content-Type值(MIME类型)

  • urlencoded 格式字符串
    application/x-www-form-urlencoded
    xxx=xxx&xxx=xxx
    qs.stringify/parse 可以实现对象和urlencoded字符串之间的转换
  • json 格式字符串
    application/json
    '{"name":"xxx", "age":xxx}'
    JSON.stringify/parse 可以实现对象和json字符串之间的转换
  • form-data 格式 一般用于上传文件
    multipart/form-data
    let formData = new FormData();
    formData.append('name', 'xxx');
    formData.append('age', 18);
    formData.append('avatar', file);
  • text/plain 普通文本
    text/plain
    xxx
  • 其他类型

Fetch请求封装

/* 
  request([config])
    + url 请求地址
    + method 请求方法 *GET, POST, PUT, DELETE, etc.
    + credentials 携带资源凭证 include, *same-origin, omit
    + headers:null 自定义的请求头信息(格式必须是纯粹对象)
    + body:null 请求主体信息(只针对于POST系列请求,根据当前服务器要求,如果用户传递的是一个纯粹对象,我们需要把其变为urlencoded格式字符串(设定请求头中的Content-Type)...)
    + params:null 设定问号传参信息(格式必须是纯粹对象,我们在内部把其拼接到url的末尾)
    + responseType 预设服务器返回结果的读取方式 *json, text, blob, arraybuffer, etc.
    + signal 中断请求的信号(基于AbortController实现中断请求)


    ------
    request.get/head/post/put/delete(url[, config]) 预先指定了配置项中的url/method
    request.post/put/delete(url[, data[, config]]) 预先指定了配置项中的url/method/body
    request.abort() 中断当前请求
*/
import qs from 'qs'
import { isPlainObject } from '@/assets/utils'
import { ElMessage } from 'element-plus'
/* 核心方法 */
const request = (config) => {
  //  init config
  if (!isPlainObject(config)) config = {}
  config = Object.assign(
    {
      url: '',
      method: 'GET',
      credentials: 'include',
      headers: null,
      body: null,
      params: null,
      responseType: 'json',
      signal: null
    },
    config
  )

  // validate config (可以给每一项都做校验)
  if (!config.url) return Promise.reject(new Error('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, responseType } = config
  // 处理URL:params存在,我们需要把params中的每一项拼接到URL末尾
  if (params) url += `${url.includes('?') ? '&' : '?'}${qs.stringify(params)}`

  // 处理请求主体:只针对于POST系列请求;body是个纯粹对象,根据当前后台请求,要把其变为urlencoded格式
  // 扩展:根据body传递格式的数据类型,在内部默认把Content-Type设置好
  if (isPlainObject(body)) {
    body = qs.stringify(body)
    headers['Content-Type'] = 'application/x-www-form-urlencoded'
  }

  // 类似于Axios的请求拦截器,例如:把存储在客户端本地的token信息携带给服务器(根据当前后台要求处理)
  let token = localStorage.getItem('token')
  if (token) headers['Authorization'] = `Bearer ${token}`

  // 发起请求
  method = method.toUpperCase()
  config = {
    method,
    credentials,
    headers,
    caches: 'no-cache',
    mode: 'cors'
  }
  if (/POST|PUT|PATCH/.test(method) && body) config.body = body
  return fetch(url, config)
    .then((res) => {
      // 成功则返回响应主体信息
      let { status, statusText } = res,
        result
      if (!/^(2|3)\d{2}$/.test(status)) return Promise.reject({ code: -1, status, statusText })
      switch (responseType.toUpperCase()) {
        case 'TEXT':
          result = res.text()
          break
        case 'BLOB':
          result = res.blob()
          break
        case 'FORMDATA':
          result = res.formData()
          break
        case 'ARRAYBUFFER':
          result = res.arrayBuffer()
          break
        default:
          result = res.json()
      }
      return result.then(null, (reason) => Promise.reject({ code: -2, reason }))
    })
    .catch((reason) => {
      // 根据不同的失败情况做不同的统一提示
      let code = reason?.code
      if (+code === -1) {
        // 状态码问题
        switch (+reason.status) {
          case 401:
            // 未授权
            ElMessage.error('未授权,请登录')
            break
          case 404:
            // 资源不存在
            ElMessage.error('资源不存在')
            break
          case 500:
            // 服务器错误
            ElMessage.error('服务器错误')
        }
      } else if (+code === -2) {
        // 数据解析失败
        ElMessage.error('数据解析失败')
      } else if (+code === 20) {
        // 请求被中断
        ElMessage.error('请求被中断')
      } else {
        // 其他错误
        ElMessage.error('请求失败')
      }
      return Promise.reject(reason)
    })
}

/* 快捷方法 */
;['GET', 'HEAD', 'DELETE', 'OPTIONS'].forEach((method) => {
  request[method.toLowerCase()] = function (url, config) {
    if (!isPlainObject(config)) config = {}
    config['url'] = url
    config['method'] = method
    return request(config)
  }
})
;['POST', 'PUT', 'PATCH'].forEach((method) => {
  request[method.toLowerCase()] = function (url, body, config) {
    if (!isPlainObject(config)) config = {}
    config['url'] = url
    config['method'] = method
    config['body'] = body
    return request(config)
  }
})

export default request

接口管理

import http from './http';
export const getUserInfo = (data) => {
  return http.get('/api/getUserInfo', data);
};

接口测试

// 在请求发送时创建请求中断器
let ct = new AbortController()
getUserInfo({signal:ct.signal})
// 中断请求
ct.abort()