Axios不行?Fetch?行!

192 阅读5分钟

通常我们使用xhr来发送http请求,也就是XMLHttpRequest,但是由于原生都是通过回调的方式获取到返回值,多次使用很容易陷入回调地狱,如下:

const xhr = new XMLHttpRequest()
xhr.onload = e => { ... }
xhr.onloadend = e => { ... }
xhr.onerror = e => { ... }
xhr.timeout = e => { ... }

当然市面上有很多基于promise封装的库,比如广为人知的axios,但是如今有浏览器原生支持的fetch对象,使得能用原生的方式更简洁的实现接口调用。首先大致了解一下fetch和传统xhr的区别:

  1. fetch()使用 Promise,不使用回调函数,因此大大简化了写法,写起来更简洁
  2. fetch()采用模块化设计,API 分散在多个对象上(Response 对象、Request 对象、Headers 对象),更合理一些;相比之下,XMLHttpRequest 的 API 设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码
  3. fetch()可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。XMLHTTPRequest 对象不支持数据流,所有的数据必须放在缓存里,不支持分块读取,必须等待全部拿到后,再一次性吐出来

 

接下来我们会基于fetch实现一个类似axios用法的简单的请求封装,以下代码部分使用es6新特性,如果有不熟悉的地方建议先学习一下语法哈~

class FetchAxios {
  constructor({ baseURL = '', timeout = 6000 }) {
    this.baseURL = baseURL
    this.timeout = timeout
    this.controller = new AbortController()
    this.headers = {}
    this.interceptors = {
      request() {},
      response() {}
   }
 }
}

首先通过class的方式创建了一个FetchAxios的类(当然function的方式也完全没有问题),其中在构造函数里初始化了请求的baseURL,超时时间,请求头和拦截器这些比较基础的设置。

在动手写实际的请求方法之前,需要了解一下fetch为我们提供了哪些参数配置。

const options = {
  method: '',
  headers: {},
  body:  undefined,
  referrer: '',
  referrerPolicy: '',
  mode: 'cors',           // 请求模式
  credentials: '',         // 是否发送cookie(跨域发送请设为 include)
  cache: 'default',         // 缓存处理方式
  redirect: 'follow',        // 指定 HTTP 跳转的处理方法
  integrity: '',          // 指定一个哈希值检查res数据是否等于这个预先设定的哈希值
  keepalive: false,         // 页面卸载时继续发送数据
  signal: undefined         // 指定一个 AbortSignal 实例,用于取消fetch请求
}
 
fetch(url, options).then(res => { ... })

fetch底层是对Request对象的一次封装,所以参数基本一致。url / methods / body 等常用的参数相信理解起来也没有什么问题,之后的部分业主要也是涉及到这三个参数,当然其他部分详细信息感兴趣的话可以至MDN Web Docs了解详细参数信息。其中部分可能在业务中使用到接下来也会稍作介绍但是不作为重点。

keepalive:一个典型的场景就是,用户离开网页时,脚本向服务器提交一些用户行为的统计信息。这时,如果不用该属性,数据可能无法发送,因为浏览器已经把页面卸载了。

window.onunload = () => {
  fetch(url, {
    keepalive: true
 })
}

signal:fetch请求发送后,如果因为各种原因想要主动取消(包括但不限于超时)需要用到AbortController对象

const controller = new AbortController()
 
fetch(url, {
  signal: controller.signal
});
 
controller.abort()

在了解了基本的原生使用方法后,就可以开始动手封装我们自己的请求部分了,但是实际封装起来会发现并没有我们想象的那么方便,主要是因为fetch还是一个比较基础的web api所以需要我们自己处理的部分实际上也不少。

request({ url, methods = 'GET', params = {}, data = {} }) {
  const getParamsStr = params => {
    const target = []
    for (const key in params) {
      target.push(`${key}=${params[key]}`)
   }
    return target.length > 0 ? `?${target.join('&')}` : ''
 }
  
  const _url = `${this.baseURL}url${getParamsStr(params)}`
  
  return new Promise((resolve, reject) => {
    fetch(_url, { methods, data }).then(res => {
      ...
      resolve(res)
   }).catch(err => {
      ...
      reject(err)
   })
 })
}

如果有小伙伴动手写到这一步,然后实例化一个对象出来以后调用request方法得到的返回值并不如我们所料,咋乱七八糟的东西这么多?因为fetch的返回值是一个Response对象,需要执行json()取出所有内容比你高转换成为json对象。此外,除非网络错误或者无法连接,并不会根据请求的状态抛出异常。而且拦截器也还么有用到,所以 我们需要对之前的代码做一点点修改:

this.interceptors.request(this)
 
fetch(_url, { methods, data }).then(response => {
  const json = response.json()
  if (response.ok && response.status >= 200 && response.status < 300) {
    this.interceptors.response(json)
    ...
    resolve(json)
 } else {
    ...
    reject(response.statusText)
 }
})

好了,至此我们就基本上完成了请求的全过程,至少后端小哥哥们写的接口已经能用起来了,但是请求还没有超时的处理,所以如果服务端出现了问题请求将会一直处于pending的状态中,最后来处理一下这个问题(虽然这已经和fetch本身没啥关系了)

overtime() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      this.controller.abort()
      reject()
   }, this.timeout)
 })
}

整理一下发现需要把超时和之前的请求拼在一起,两个Promise先执行完就结束了,另一个不会继续执行,于是乎想到了Promise.race,当然这个平时在业务中可能接触的并不多,更了解的还是Promise.all(如果这个也不太了解的话强烈建议去学习一下)特别是已经用上async/await的小伙伴,学完回头再看之前的代码可能会有不少可以优化的部分,用同步的代码执行异步的事情是很容易出现问题的。言归正传我们迅速收尾:

return Promise.race([this.request(options), this.overtime()])

 

最后我们可能还是会发现axios真香,毕竟更成熟,使用更方便,社区也很完善,而且也提供了一些fetch不支持的功能比如上传下载进度等等,但是!我们有梦想的程序员不甘心局限于业务嘛,这还是提供了一个新思路。当然我个人还是比较倾向于拥抱新技术,毕竟前端发展这么快,保持一颗学习的心嘛~

 

参考

阮一峰的网络日志 Fetch API

使用 Fetch

netword requests fetch