前端网络基础 - fetch

目录

XMLHttpRequest缺点

fetch的优点

fetch的请求和响应设计

Request

Response

fetch函数的用法

fetch取消请求

fetch的异常结果

fetch和axios的区别


XMLHttpRequest缺点

浏览器提供了原生的AJAX实现类XMLHttpRequest,基于该类实例,我们可以实现在网页上发送AJAX请求到服务端。

但是XMLHttpRequest的设计并不完美,主要体现在以下几个方面:

  • HTTP请求,响应都被耦合在XMLHttpRequest实例上,结构不够简单明了
  • 采用事件回调的方式获取HTTP响应,可能会产生回调地狱
  • 如果HTTP响应数据过大,则会占用大量内存
  • 最后一点就是,XMLHttpRequest实现AJAX的步骤太零碎了

我们分别举例说明下


   
   
  1. const xhr = new XMLHttpRequest()
  2. xhr. open( 'post', 'http://localhost:3000/test')
  3. xhr. setRequestHeader( 'Content-Type', 'application/json')
  4. xhr. responseType = 'json'
  5. xhr. send( JSON. stringify({
  6. name: 'qfc',
  7. age: 18
  8. }))
  9. xhr. onreadystatechange = function( ){
  10. if(xhr. readyState === 4){
  11. if(xhr. status >= 200 && xhr. status < 300){
  12. console. log(xhr. status)
  13. console. log(xhr. statusText)
  14. console. log(xhr. getAllResponseHeaders())
  15. console. log(xhr. response)
  16. }
  17. }
  18. }

通过以上代码,我们发现,HTTP请求URL,请求METHOD,请求HEAD,请求BODY,全部依赖于xhr的方法进行设置,HTTP响应状态码,状态描述,响应HEAD,响应BODY,也全部依赖于xhr来获取,这其实不符合高内聚,低耦合要求,我们期望将HTTP请求所有的信息封装在一起,将HTTP响应的所有信息封装在一起。


   
   
  1. // 下面案例是 先查询订单123的信息,再根据订单123信息中productId去查询商品信息
  2. const xhr = new XMLHttpRequest()
  3. xhr. open( 'get', 'http://localhost:3000/order/123')
  4. xhr. responseType = 'json'
  5. xhr. send()
  6. xhr. onreadystatechange = function( ){
  7. if(xhr. readyState === 4){
  8. if(xhr. status >= 200 && xhr. status < 300){
  9. const xhr2 = new XMLHttpRequest()
  10. xhr2. open( 'get', `http://localhost:3000/product/${xhr.response.productId}`)
  11. xhr2. responseType = 'json'
  12. xhr2. send()
  13. xhr2. onreadystatechange = function( ){
  14. if(xhr2. readyState === 4) {
  15. if(xhr2. status >= 200 && xhr2. status < 300) {
  16. console. log(xhr2. response)
  17. }
  18. }
  19. }
  20. }
  21. }
  22. }

上面代码就是典型的回调地狱式的异步串行案例。我们期望基于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响应


   
   
  1. fetch( 'http://localhost:3000/test'). then( response => { // fetch函数返回值是一个Promise类型对象
  2. console. log(response) // 该Promise对象的结果值response就是HTTP响应
  3. })

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是可以选参数,需要传入一个对象,对象可以包含如下属性

methodHTTP请求方法
headersHTTP请求头
bodyHTTP请求体。可以是BlobBufferSource (en-US)FormDataURLSearchParamsUSVString,或ReadableStream对象。
mode

请求的模式

  • cors:默认值,允许跨域请求。
  • same-origin:只允许同源请求。
  • no-cors:请求方法只限于 GET、POST 和 HEAD,并且只能使用有限的几个简单标头,不能添加跨域的复杂标头,相当于提交表单所能发出的请求。
credentials

是否发送 Cookie

  • same-origin:默认值,同源请求时发送 Cookie,跨域请求时不发送。
  • include:不管同源请求,还是跨域请求,一律发送 Cookie。
  • omit:一律不发送。
cache

HTTP缓存设置

  • default:默认值,先在缓存里面寻找匹配的请求。
  • no-store:直接请求远程服务器,并且不更新缓存。
  • reload:直接请求远程服务器,并且更新缓存。
  • no-cache:将服务器资源跟本地缓存进行比较,有新的版本才使用服务器资源,否则使用缓存。
  • force-cache:缓存优先,只有不存在缓存的情况下,才请求远程服务器。
  • only-if-cached:只检查缓存,如果缓存里面不存在,将返回504错误。
redirect对重定向处理的模式: followerror, or manual。在Chrome中,Chrome 47 之前的版本默认值为 manual ,自Chrome 47起,默认值为follow。
referrer 用于设定fetch()请求的referer标头
referrerPolicy

用于设定Referer标头的规则

  • no-referrer-when-downgrade:默认值,总是发送Referer标头,除非从 HTTPS 页面请求 HTTP 资源时不发送。
  • no-referrer:不发送Referer标头。
  • originReferer标头只包含域名,不包含完整的路径。
  • origin-when-cross-origin:同源请求Referer标头包含完整的路径,跨域请求只包含域名。
  • same-origin:跨域请求不发送Referer,同源请求发送。
  • strict-originReferer标头只包含域名,HTTPS 页面请求 HTTP 资源时不发送Referer标头。
  • strict-origin-when-cross-origin:同源请求时Referer标头包含完整路径,跨域请求时只包含域名,HTTPS 页面请求 HTTP 资源时不发送该标头。
  • unsafe-url:不管什么情况,总是发送Referer标头。
intergrity 指定一个哈希值,用于检查 HTTP 回应传回的数据是否等于这个预先设定的哈希值。比如,下载文件时,检查文件的 SHA-256 哈希值是否相符,确保没有被篡改
signal指定一个 AbortSignal 实例,用于取消fetch()请求
keepalive用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据。

   
   
  1. const req = new Request( 'http://localhost:3000/test', {
  2. method: 'post',
  3. headers: {
  4. 'Content-Type': 'application/json'
  5. },
  6. body: JSON. stringify({
  7. name: 'qfc',
  8. age: 18
  9. })
  10. })
  11. fetch(req). then( res => {
  12. console. log(res)
  13. })

其中需要注意的是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实例具有以下属性

statusHTTP响应状态码
statusTextHTTP响应状态描述
headers

HTTP响应头,headers无法直接通过.来获取响应头,而要通过get方法来获取,原因时headers是Header类型,该类实现了Symbol.iterator,是一个可迭代对象,他需要通过get方法获取指定响应头

 

bodyHTTP响应体。一个简单的 getter,用于暴露一个 ReadableStream 类型的 body 内容。
bodyUsed包含了一个布尔值 (en-US)来标示该 Response 是否读取过 Body
ok

本次响应是否成功,true成功,false失败。

判断标准是:HTTP响应状态码在200~299之间表示成功,其他表示失败

type

响应类型,有如下值:

  • basic: 标准值, 同源响应, 带有所有的头部信息除了“Set-Cookie” 和 “Set-Cookie2″.
  • cors: Response 接收到一个有效的跨域请求. 
  • error: 网络错误. 没有有用的描述错误的信息。响应的状态为0,header为空且不可变。从 Response.error()中获得的响应的类型.
  • opaque: 响应 “no-cors” 的跨域请求.
urlHTTP请求URL
redirected表示该 Response 是否来自一个重定向,如果是的话,它的 URL 列表将会有多个条目。

其中,我们需要注意的是body属性值是一个可读流,所以我们无法直接获取body内容,需要从可读流中读取内容,而读取可读流中内容也是一个异步操作,Response贴心的为我们提供了如下实例方法去异步地获取body可读流中的内容

json()读取body内容为JSON对象
text()读取body内容为普通文本字符串
formData()读取body内容为FormData对象
blob()读取body内容为Blob对象
arrayBuffer()读取body内容为ArrayBuffer对象

以上方法都返回一个Promise对象,且Promise对象的结果值为它们读取到并转换为对应格式的数据。


   
   
  1. async function test( ){
  2. const response = await fetch( 'http://localhost:3000/test?name=qfc&age=18')
  3. console. log( 'bodyUsed:', response. bodyUsed)
  4. const body = await response. json()
  5. console. log(body)
  6. console. log( 'bodyUsed:', response. bodyUsed)
  7. const bodyAgain = await response. json()
  8. console. log(bodyAgain)
  9. }
  10. test()

通过以上代码测试发现,当response.json()返回的Promise的结果值确实是body实际内容,并且自动被转化为JSON对象。bodyUsed属性在json()执行后,也从false改变为了true,表示body内容读取过了。

需要注意的是,可读流的内容只能读取一次,读取完就没了,再次读取则会报错

如果我们想进行多次读取,则可以对可读流进行克隆,然后操作克隆的可读流,具体操作如下:


   
   
  1. async function test( ){
  2. const response = await fetch( 'http://localhost:3000/test?name=qfc&age=18')
  3. const clone1 = response. clone()
  4. const body = await clone1. json()
  5. console. log(body)
  6. const clone2 = response. clone()
  7. const bodyAgain = await clone2. json()
  8. console. log(bodyAgain)
  9. }
  10. test()

fetch函数的用法

前面介绍了Request和Response,我们知道了fetch可以入参一个Request对象,返回一个Response对象为结果值的Promise对象。

但是每次执行fetch,都创建一个Request对象显得有点麻烦,所以fetch函数支持如下语法:

Promise<Response> fetch(input[, init]);

   
   
  1. fetch( 'http://localhost:3000/test', {
  2. method: 'post',
  3. headers: {
  4. 'Content-Type': 'application/json'
  5. },
  6. body: JSON. stringify({
  7. name: 'qfc',
  8. age: 18
  9. })
  10. }). then( response => {
  11. console. log(response)
  12. })

即,fetch不需要入参一个标准的Request对象,而是将用于创建Request对象的入参转移到fetch函数的入参。

fetch取消请求

不同于XMLHttpRequest基于实例来取消请求,由于fetch没有提供发送AJAX的实例,所以fetch需要通过传入配置的方式,建立与底层发送AJAX的实例的联系。

浏览器原生提供了一个AbortController类,该类的原型上有一个signal属性,有一个abort函数

而fetch函数的第二个参数配置对象有一个属性signal,该属性用于接收一个AbortController实例的signal属性值,这也建立了fetch函数底层发送AJAX的实例与AbortController实例的联系。

而Abort实例通过调用abort即可引发fetch函数发送的AJAX取消。 


   
   
  1. const controller = new AbortController()
  2. fetch( 'http://localhost:3000/test', {
  3. signal: controller. signal
  4. }). then( response => { console. log(response)})
  5. . catch( err => { console. log( '错误信息:', err)})
  6. 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再将得到的取消请求控制权再次交给用户,由用户把控取消请求

转发自:https://blog.csdn.net/qfc_128220/article/details/123049684

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值