通常我们使用xhr来发送http请求,也就是XMLHttpRequest,但是由于原生都是通过回调的方式获取到返回值,多次使用很容易陷入回调地狱,如下:
const xhr = new XMLHttpRequest()
xhr.onload = e => { ... }
xhr.onloadend = e => { ... }
xhr.onerror = e => { ... }
xhr.timeout = e => { ... }
当然市面上有很多基于promise封装的库,比如广为人知的axios,但是如今有浏览器原生支持的fetch对象,使得能用原生的方式更简洁的实现接口调用。首先大致了解一下fetch和传统xhr的区别:
- fetch()使用 Promise,不使用回调函数,因此大大简化了写法,写起来更简洁
- fetch()采用模块化设计,API 分散在多个对象上(Response 对象、Request 对象、Headers 对象),更合理一些;相比之下,XMLHttpRequest 的 API 设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码
- 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不支持的功能比如上传下载进度等等,但是!我们有梦想的程序员不甘心局限于业务嘛,这还是提供了一个新思路。当然我个人还是比较倾向于拥抱新技术,毕竟前端发展这么快,保持一颗学习的心嘛~
参考