背景
之前看过很多 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 的文章。这样好方便我以后总结相关的内容以飨读者。
参考
xmlhttprequest-vs-the-fetch-api-whats-best-for-ajax-in-2019
open-pdf-downloaded-api-javascript