从 fetch 说开,到 blob

4,937 阅读6分钟

背景

之前看过很多 fetch 相关的文章,说的都是 fetch 如何使用,以及 fetch 的优势。但是最近也发现 fetch 有很多坑。

前两天有个上传文件的需求,需要在页面上显示上传文件的进度条。之前产品线做过类似的内容,用的是 axios,通过 onUploadProgress 回调函数来获取上传进度。这次做需求的时候发现,新的产品线用的是 fetch 来处理请求。然后就发现 fetch 不支持上传进度获取。之前对 fetch 和 XMLHttpRequest 之间没有太多对比,所以这次就借机会重新了解了下两者的区别。

XHR vs Fetch

首先 fetch 和 XMLHttpRequest 都是 AJAX 技术,都是不刷新页面获取后端数据,然后在前端更新页面的方式。然后 fetch 的出现,一定程度上取代了 XMLHttpRequest。因为 fetch 的写法更加简洁,优雅,并且通过链式调用的方式而不是回调的方式处理响应。看起来 fetch 优势明显。但是 fetch 却也有很多问题。或者说有很多无法取代 XMLHttpRequest 的地方。

1. 浏览器支持:“古老”的 XMLHttpRequest 在浏览器支持方面明显有优势。fetch 则主要支持 2017 年之后的浏览器版本。因此在选择的时候一定要考虑用户的情况,再决定选择哪种请求方式。

2. cookie:fetch 向服务器发送请求的时候默认不发送 cookies,通过添加 credentials 来发送 cookies。

fetch(
    'http://domain/service',
    {
      method: 'GET',
      credentials: 'same-origin'
    }
)

3. 后端错误无法弹出:fetch 在处理 404 或者 500 错误的时候不会触发 reject,进而进入到 catch() 语句中。只有当网络错误或者其他请求无法完成的时候才会触发 reject 状态,进入到 catch 语句。因此使用 fetch 的时候,对错误处理的方式会更加复杂。

4. timeout:XMLHttpRequest 可以通过下面的方式添加 timeout。

// set timeout
xhr.timeout = 3000; // 3 seconds
xhr.ontimeout = () => console.log('timeout', xhr.responseURL);

但是原生的 fetch 却不支持 timeout,因此请求的处理时长完全交给浏览器,这个不确定性就很大。所以为了让 fetch 支持 timeout 只能通过一些 trick 的方式。下面是两种 trick 的方式来给 fetch 添加 timeout。

// 方式一,添加 setTimeout 来添加 timeout
function fetchTimeout(url, init, timeout = 3000) {
  return new Promise((resolve, reject) => {
    fetch(url, init)
      .then(resolve)
      .catch(reject);
    setTimeout(reject, timeout);
  })
}

// 方式二,通过 Promise.race 来添加 timeout
Promise.race([
    fetch('http://url', { method: 'GET' }),
    new Promise(resolve => setTimeout(resolve, 3000))
])
.then(response => console.log(response))

5. 取消请求:XMLHttpRequest 可以通过 xhr.abort() 取消请求,还可以添加 onabort 回调来处理 abort 的情况。而从 fetch 诞生起,很长一段时间原生的 fetch 是不支持 abort 的。现在可以 abort 了,但是代码却也很麻烦。必须要用到 AbortController API 。代码示例如下:

const controller = new AbortController();

fetch(
  'http://domain/service',
  {
    method: 'GET'
    signal: controller.signal
  })
  .then( response => response.json() )
  .then( json => console.log(json) )
  .catch( error => console.error('Error:', error) );

调用 controller.abort() 可以取消请求。Promise 会 reject,会进入 .catch() 语句。

6. progress:目前而言,fetch 不支持获取 progress。因此无法获取文件的上传进度。

所以就目前而言,如果需求复杂的话,其实不建议用 fetch。因为 fetch 还是一个比较新的请求方式,还有很多不完善的地方。

鉴于无法实现 progress,所以需求让实现进度条就只能做个假的进度条了。那么为什么又会说到 blob 呢?

如何通过 fetch 将返回的 json 数据变成 json 格式文件下载下来

做完上传文件的需求,又来了个下载文件的需求。需求是要求点击按钮,下载一个 json 格式的文件,如果后端接口是 GET 请求的话,直接写成 <a href='xxx'> 的形式就行了。但是后端给的接口是 post 接口,然后返回一段 json 数据。还是 fetch 的请求方式,要求把这段 json 格式的数据变成一个可下载的 json 文件让用户下载下来。查了资料之后看到下面这样一段代码。试了下果然可以。

showFile(blob){
   // It is necessary to create a new blob object with mime-type explicitly set
   // otherwise only Chrome works like it should
   var newBlob = new Blob([blob], {type: "application/pdf"})

   // IE doesn't allow using a blob object directly as link href
   // instead it is necessary to use msSaveOrOpenBlob
   if (window.navigator && window.navigator.msSaveOrOpenBlob) {
     window.navigator.msSaveOrOpenBlob(newBlob);
     return;
   }

   // For other browsers:
   // Create a link pointing to the ObjectURL containing the blob.
   const data = window.URL.createObjectURL(newBlob);
   var link = document.createElement('a');
   link.href = data;
   link.download="file.pdf";
   link.click();
   setTimeout(function(){
     // For Firefox it is necessary to delay revoking the ObjectURL
     window.URL.revokeObjectURL(data);
   }, 100);
 }

 fetch([url to fetch], {[options setting custom http-headers]})
   .then(r => r.blob())
   .then(showFile)

这段代码中有几个点说一下。

1. 文件类型:首先这篇文章中作者是要下载一个 pdf 文件。所以 new Blob() 的时候 type 就是 application/pdf。正常情况下只要写成 type: blob.type 就可以了,blob 天然会携带这个属性。如果你明确知道 type,那么写死也可以的。

2. response.blob():说实话以前所有的返回首先用 response.json() 的方式进行,从来没用过 response.blob(),这是第一次见到。具体关于 response 还有什么其他方法,参考 MDN-Response-Methods。这个方法返回的是一个 blob 对象。blob 是一个类文件对象。然后通过 URL.createObjectURL(blob) 创建这个对象(或者说文件)的 url,添加给 <a> 标签,然后通过模拟点击的 click() 方法实现下载。这里下载文件名称也可以自定义的,代码中是 file.pdf,可以根据需求修改成想要的文件名。最后记得通过 window.URL.revokeObjectURL(data) 来释放生成的对象 URL,这样浏览器可以解除文件的引用,进而让文件被垃圾回收机制回收,避免内存泄漏。

深入理解 Blob

做完这个需求,就不得不说说 blob。但是说到 blob,我一直对于 blob 的理解不是很深入。很久之前自己玩得时候做过一个网页音乐播放器,用 canvas 做出一个跳动的彩色 bar 来显示音频的波动,里面就用到过 Blob 和 uintArray。这次做完这个需求对于 blob 的理解深入了一些,但是还是不是很清楚。正好前段时间看过一位大佬写过一篇文章。我觉得对于理解 blob 有帮助。就把文章链接贴下来,大家自行体会吧。

为什么视频网站的视频链接地址是blob?

欢迎大佬在评论中扔给我深入理解 blob 的文章。这样好方便我以后总结相关的内容以飨读者。

参考

xmlhttprequest-vs-the-fetch-api-whats-best-for-ajax-in-2019

open-pdf-downloaded-api-javascript

为什么视频网站的视频链接地址是blob?