当前位置: 首页 > 科技观察

这个文件下载问题难倒了至少三位同事

时间:2023-03-18 20:43:49 科技观察

今天给大家分享两个有用的浏览器行为,与预期不符。这两个问题其实都不是什么难的问题,但是我在工作中发现了很多。人们被难住了。在我的印象中,群里至少有3位同事问过这样的问题,还有一位同事上周也被这个现象坑过,所以我觉得这应该是一个普遍的问题,在此分享给大家。希望对大家有所帮助。现象1.点击按钮无法实现文件下载。前端同事反映,在浏览器中点击实现的“下载产品图片”按钮,却无法下载(应该是下载zip文件)img。在栏中输入这个下载地址,但可以直接从浏览器下载。为什么?我们可以打开调试工具“网络部分”,然后点击上面的“下载产品图片”,首先检查网络请求是否正常。1、首先看请求头,可以看到状态码是200,响应头有两个,content-disposition和Content-Type。画外音:Content-Type:application/octet-stream告诉客户端这是一个二进制文件,content-disposition告诉客户端这是一个需要下载的附件,并告诉浏览器附件的默认文件名。2、检查这个请求的responsebody和步骤1中的application/octet-stream是否一致:img可以看到response是一堆乱码,是文件的二进制流表示,所以实际上是从请求的角度来看没有问题,文件正常返回,但是为什么没有下载文件,下载的文件到哪里去了,注意上图中另一个红框的XHR,它的全称是XMLHttpRequest,也就是ajax请求表单的一种表现形式。Ajax本身不能触发浏览器的下载功能,其响应将由JavaScript处理。使用ajax下载完成后,response以字符串的形式存在内存中,那么是否可以使用ajax下载呢?不行,看看为什么浏览器可以下载。我们发现浏览器的GET请求(主要加载frame,标签点击触发)或者POST请求(以form形式存在)都可以下载文件,因为这是浏览器的内置事件,下载的响应会被处理由浏览器本身。如果浏览器识别出它是二进制流数据,就会下载它。如果它识别出是可以打开的文件,比如xml、image等,则不会下载。它将使用预览样式存在。那么为什么ajax默认不能下载文件呢?这是受浏览器的安全策略限制的。试想一下,如果ajax可以下载文件,就意味着ajax可以直接与磁盘进行交互,这会带来严重的安全隐患。根据上面的分析,我们有了一个使用ajax下载文件的想法。既然使用标签(或框架)的点击事件可以触发浏览器内置的下载行为,那么我们可以在下载完之后用ajax用js获取响应。新建一个隐藏的a标签(标签的href指向文件的链接),执行它的点击事件,触发浏览器内置的下载事件,文件就可以下载了,但是需要注意的就是在创建的标签中添加一个下载属性。.这个下载属性有什么用?对于html、xml等浏览器可以打开的文件,如果不添加下载,点击a标签不会下载,而是打开。(注意download属性目前只兼容Firefox和Google)使用ajax下载文件的代码示例如下:constfilename=response.headers['content-disposition'].match(/filename=(.*)/)[1]//首先创建一个Blob对象(代表不可变的原始数据的类文件对象)constblob=newBlob([response.data],{type:'application/zip'});if(typeofwindow.navigator.msSaveBlob!=='undefined'){//兼容IE,window.navigator.msSaveBlob:将文件保存到本地window.navigator.msSaveBlob(blob,decodeURI(filename))}else{letelink=document.createElement("a");//创建一个标签elink.style.display="none";//隐藏标签elink.href=window.URL.createObjectURL(blob);//配置href指向内存地址thelocalfileelink.download=filename;elink.click();URL.revokeObjectURL(elink.href);//释放URL对象document.body.removeChild(elink);//移除标签}现象2:在浏览器中输入图片链接,想预览,但是原来是下载图片的问题。其实经过上面的分析,相信大家也很容易猜到是怎么回事了。我们先看一下数据包:img可以看到返回的Content-Type是octet-stream,上面我们提到它指的是任意类型的二进制流数据。通常,下载的文件返回此类型。由于浏览器无法识别和打开流数据,它会下载它。那为什么大部分图片都可以在浏览器上预览呢,因为它返回的Content-Type是image/png或者image/jpeg,浏览器可以直接识别打开的文件,这样就不会执行下载事件了。总结以上两个问题,需要了解浏览器的工作机制和HTTP协议。当然所以基础真的很重要,不然你查了半天可能无从下手,但是如果你知道这些原理,抓个包分析一下他们的Content-Type,你就会豁然开朗!另外,对于一些疑难杂症,了解HTTP协议和浏览器的工作机制,也有助于快速定位和解决问题。比如上图的方案中,我们使用content-disposition获取文件名constfilename=response.headers['content-disposition'].match(/filename=(.*)/)[1]但是一开始,我发现这段代码有问题。打印log发现response.headers['content-disposition']其实是空的,但是打开浏览器服务器的网络会发现content-disposition明明是存在img中的,为什么我获取不到响应标题中的内容处置?查了一下,原来是HTTP协议的问题。默认情况下,只有七个简单的响应头(simpleresponseheaders)可以暴露给外界:Cache-ControlContent-LanguageContent-LengthContent-TypeExpiresLast-ModifiedPragma。客户端(如Chrome)可以访问它,要么在网络中看到它,要么在代码中获取它们的值。而content-disposition不在其中,所以即使服务器在协议回复包中添加了这个字段,如下response.setHeader("content-disposition","attachment;filename="+filename);但因为没有“暴露”在外面,所以客户最后“看得见,吃不着”。响应头Access-Control-Expose-Headers是控制“暴露”的开关,它列出了哪些头可以作为响应的一部分暴露给外部。所以如果想让客户端访问其他的header信息,服务端不仅要在header中添加header,还要在Access-Control-Expose-Headers中列出,如下:response.setHeader("Access-Control-Expose-Headers","Content-Disposition");response.setHeader("content-disposition","attachment;filename="+filename);这样JS响应头就有了content-disposition的值。