目录
XMLHttpRequest缺点
浏览器提供了原生的AJAX实现类XMLHttpRequest,基于该类实例,我们可以实现在网页上发送AJAX请求到服务端。
但是XMLHttpRequest的设计并不完美,主要体现在以下几个方面:
- HTTP请求,响应都被耦合在XMLHttpRequest实例上,结构不够简单明了
- 采用事件回调的方式获取HTTP响应,可能会产生回调地狱
- 如果HTTP响应数据过大,则会占用大量内存
- 最后一点就是,XMLHttpRequest实现AJAX的步骤太零碎了
我们分别举例说明下
const xhr = new XMLHttpRequest()
xhr.open('post', 'http://localhost:3000/test')
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.responseType = 'json'
xhr.send(JSON.stringify({
name: 'qfc',
age: 18
}))
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
if(xhr.status >= 200 && xhr.status < 300){
console.log(xhr.status)
console.log(xhr.statusText)
console.log(xhr.getAllResponseHeaders())
console.log(xhr.response)
}
}
}
通过以上代码,我们发现,HTTP请求URL,请求METHOD,请求HEAD,请求BODY,全部依赖于xhr的方法进行设置,HTTP响应状态码,状态描述,响应HEAD,响应BODY,也全部依赖于xhr来获取,这其实不符合高内聚,低耦合要求,我们期望将HTTP请求所有的信息封装在一起,将HTTP响应的所有信息封装在一起。
// 下面案例是 先查询订单123的信息,再根据订单123信息中productId去查询商品信息
const xhr = new XMLHttpRequest()
xhr.open('get', 'http://localhost:3000/order/123')
xhr.responseType = 'json'
xhr.send()
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
if(xhr.status >= 200 && xhr.status < 300){
const xhr2 = new XMLHttpRequest()
xhr2.open('get', `http://localhost:3000/product/${xhr.response.productId}`)
xhr2.responseType = 'json'
xhr2.send()
xhr2.onreadystatechange = function(){
if(xhr2.readyState === 4) {
if(xhr2.status >= 200 && xhr2.status < 300) {
console.log(xhr2.response)
}
}
}
}
}
}
上面代码就是典型的回调地狱式的异步串行案例。我们期望基于Promise#then的链式串行,或者更进一步的async await异步任务同步化执行。
XMLHttpRequest实例自身有一个属性readyState,该属性有如下几个值
- 0:xhr实例创建
- 1:xhr.open调用
- 2:xhr.send调用
- 3:xhr收到部分HTTP响应
- 4:xhr收到全部HTTP响应
而当xhr.readyState属性值改变时,就会触发xhr.onreadystatechange事件,我们通过监听该事件,就可以知道HTTP响应是否已被收到,收到的HTTP响应会被挂载到xhr实例上,我们可以通过xhr.status,xhr.statusText,xhr.getAllResponseHeaders(),xhr.response来获得HTTP响应信息。
但是xhr并没有实现流式获取HTTP响应,即无法分块获取HTTP响应,当HTTP响应过大时,需要占用对应大小的内存,将HTTP响应全部缓存再内存中,这是内存不友好的。
fetch的优点
fetch和XMLHttpRequest一样,也是浏览器原生的,用于发送AJAX请求。
但是fetch是在XMLHttpRequest之后诞生的,它旨在解决XMLHttpRequest的不足,所以XMLHttpRequest的缺点就是它的优点,具体优点如下
- 语法简单,结构清晰明了
- 支持Promise获取异步的HTTP响应
- HTTP响应支持流式获取,内存友好
fetch被设计为函数,通过fetch函数调用即可发起AJAX,而不需要像XMLHttpRequest那样创建实例,然后基于xhr实例发起AJAX。
fetch('http://localhost:3000/test') // fetch函数调用即发起AJAX
fetch函数返回一个Promise对象,而Promise对象的结果值就是HTTP响应
fetch('http://localhost:3000/test').then(response => { // fetch函数返回值是一个Promise类型对象
console.log(response) // 该Promise对象的结果值response就是HTTP响应
})
fetch函数返回的Promise对象的结果值HTTP响应是流式获取,即使HTTP响应数据很大,也不会占用过多的内存。
fetch的请求和响应设计
fetch将HTTP请求信息封装在一个Request类中,将HTTP响应封装在一个Response类中,Request和Response类都是浏览器原生的,我们可以直接使用。
例如:fetch函数返回的Promise对象的结果值response就是Response类的实例
例如:我们可以创建一个Request对象,作为fetch函数入参
下面将详细介绍Request,Response
Request
Request() - Web API 接口参考 | MDN (mozilla.org)
var myRequest = new Request(input[, init]);
Request构造函数语法如上
input是必选参数,一般传入URL字符串,如'http://localhost:3000/test',或者传入一个Request对象
init是可以选参数,需要传入一个对象,对象可以包含如下属性
method | HTTP请求方法 |
headers | HTTP请求头 |
body | HTTP请求体。可以是Blob, BufferSource (en-US), FormData, URLSearchParams, USVString,或ReadableStream对象。 |
mode | 请求的模式
|
credentials | 是否发送 Cookie
|
cache | HTTP缓存设置
|
redirect | 对重定向处理的模式: follow , error , or manual 。在Chrome中,Chrome 47 之前的版本默认值为 manual ,自Chrome 47起,默认值为follow。 |
referrer | 用于设定fetch() 请求的referer 标头 |
referrerPolicy | 用于设定
|
intergrity | 指定一个哈希值,用于检查 HTTP 回应传回的数据是否等于这个预先设定的哈希值。比如,下载文件时,检查文件的 SHA-256 哈希值是否相符,确保没有被篡改 |
signal | 指定一个 AbortSignal 实例,用于取消fetch() 请求 |
keepalive | 用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据。 |
const req = new Request('http://localhost:3000/test', {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'qfc',
age: 18
})
})
fetch(req).then(res => {
console.log(res)
})
其中需要注意的是Request对象的body属性,该属性值支持
- 查询参数字符串,如'name=qfc&age=18'
- 文本字符串,如'{"name":"qfc", "age": 18}'
- FormData对象
- Blob对象
- ReadableStream对象
- BufferSource对象
居然不支持普通JS对象,这让我过于意外,在如今application/json数据格式的天下,居然不默认支持将JS对象自动转为JSON字符串...
body传入普通JS对象,服务器直接报错了,因为服务器收到的是一个JS对象(二进制类型),而不是一个JSON字符串(文本类型),所以无法进行JSON解析,所以报错了。
Response
Response - Web API 接口参考 | MDN (mozilla.org)
通常情况下,我们不手动构造一个Response实例,我们只需要了解Response的结构即可。
Response实例具有以下属性
status | HTTP响应状态码 |
statusText | HTTP响应状态描述 |
headers | HTTP响应头,headers无法直接通过.来获取响应头,而要通过get方法来获取,原因时headers是Header类型,该类实现了Symbol.iterator,是一个可迭代对象,他需要通过get方法获取指定响应头
|
body | HTTP响应体。一个简单的 getter,用于暴露一个 ReadableStream 类型的 body 内容。 |
bodyUsed | 包含了一个布尔值 (en-US)来标示该 Response 是否读取过 Body 。 |
ok | 本次响应是否成功,true成功,false失败。 判断标准是:HTTP响应状态码在200~299之间表示成功,其他表示失败 |
type | 响应类型,有如下值:
|
url | HTTP请求URL |
redirected | 表示该 Response 是否来自一个重定向,如果是的话,它的 URL 列表将会有多个条目。 |
其中,我们需要注意的是body属性值是一个可读流,所以我们无法直接获取body内容,需要从可读流中读取内容,而读取可读流中内容也是一个异步操作,Response贴心的为我们提供了如下实例方法去异步地获取body可读流中的内容
json() | 读取body内容为JSON对象 |
text() | 读取body内容为普通文本字符串 |
formData() | 读取body内容为FormData对象 |
blob() | 读取body内容为Blob对象 |
arrayBuffer() | 读取body内容为ArrayBuffer对象 |
以上方法都返回一个Promise对象,且Promise对象的结果值为它们读取到并转换为对应格式的数据。
async function test(){
const response = await fetch('http://localhost:3000/test?name=qfc&age=18')
console.log('bodyUsed:', response.bodyUsed)
const body = await response.json()
console.log(body)
console.log('bodyUsed:', response.bodyUsed)
const bodyAgain = await response.json()
console.log(bodyAgain)
}
test()
通过以上代码测试发现,当response.json()返回的Promise的结果值确实是body实际内容,并且自动被转化为JSON对象。bodyUsed属性在json()执行后,也从false改变为了true,表示body内容读取过了。
需要注意的是,可读流的内容只能读取一次,读取完就没了,再次读取则会报错
如果我们想进行多次读取,则可以对可读流进行克隆,然后操作克隆的可读流,具体操作如下:
async function test(){
const response = await fetch('http://localhost:3000/test?name=qfc&age=18')
const clone1 = response.clone()
const body = await clone1.json()
console.log(body)
const clone2 = response.clone()
const bodyAgain = await clone2.json()
console.log(bodyAgain)
}
test()
fetch函数的用法
前面介绍了Request和Response,我们知道了fetch可以入参一个Request对象,返回一个Response对象为结果值的Promise对象。
但是每次执行fetch,都创建一个Request对象显得有点麻烦,所以fetch函数支持如下语法:
Promise<Response> fetch(input[, init]);
fetch('http://localhost:3000/test', {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'qfc',
age: 18
})
}).then(response => {
console.log(response)
})
即,fetch不需要入参一个标准的Request对象,而是将用于创建Request对象的入参转移到fetch函数的入参。
fetch取消请求
不同于XMLHttpRequest基于实例来取消请求,由于fetch没有提供发送AJAX的实例,所以fetch需要通过传入配置的方式,建立与底层发送AJAX的实例的联系。
浏览器原生提供了一个AbortController类,该类的原型上有一个signal属性,有一个abort函数
而fetch函数的第二个参数配置对象有一个属性signal,该属性用于接收一个AbortController实例的signal属性值,这也建立了fetch函数底层发送AJAX的实例与AbortController实例的联系。
而Abort实例通过调用abort即可引发fetch函数发送的AJAX取消。
const controller = new AbortController()
fetch('http://localhost:3000/test', {
signal: controller.signal
}).then(response => {console.log(response)})
.catch(err => {console.log('错误信息:', err)})
setTimeout(()=>{controller.abort()}, 1000) // 服务器5s后返回
但是有一个很奇怪的地方,controller.abort是一个函数,居然不能直接作为setTimeout回调,而是需要封装到一个函数中,不知道为啥
而当controller.abort()执行后,fetch就会抛出一个DOMException,异常的name属性为AbortError
fetch的异常结果
fetch只将网络异常,如网络未接入,网络中断,服务器无法连接,当成真异常,而对于HTTP响应状态码不在200~299的情况都会被当成正常结果,而不是异常。
我们再来看下异常响应的结构
可以发现,fetch异常响应结果值就是一个TypeError对象,该对象自身有message,stack属性,原型上有一个name属性
fetch和axios的区别
二者最本质区别是
fetch是浏览器原生函数,axios是基于浏览器原生XMLhttpRequest封装的第三方库。
在请求设计上
fetch只能当成函数使用,axios不仅可以当成函数使用,也可以当成对象使用
fetch函数第一个入参不能是配置对象,axios可以是
fetch函数入参配置对象中请求体body不能直接传入普通JS对象,axios入参配置对象的data可以
在正常结果上
fetch返回的Promise结果值Response实例的body是一个可读流对象,需要异步读取
axios返回的Promise结果值中所有属性都可以同步获取
在异常结果上
fetch只将网络异常当成异常,对于服务器返回非 200~299 的HTTP响应,都看出正常结果
axios默认将非 200~299 的HTTP响应,看出是异常结果
另外fetch的异常结果对象有两种TypeError,DOMException,都是浏览器原生异常对象
而axios的异常对象都是自定义的对象,包含详细的异常信息
在取消请求上
fetch依赖于外部的AbortController实例来取消请求,通过AbortController实例的signal和fetch入参配置对象属性signal建立联系,然后通过AbortController实例调用abort方法完成取消请求。
axios依赖于外部的axios.CancelToken实例来取消请求,通过axios.CancelToken实例与axios入参配置对象属性cancelToken建立联系,然后axios内部将xhr.abort()执行权交给axios.CancelToken实例,通过axios.CancelToken再将得到的取消请求控制权再次交给用户,由用户把控取消请求