最近在做一个文件下载的功能,但是对前端下载方式并不了解,以前做过类似的,都是通过超链接的src属性去访问后端,再配置一个download属性,回来的数据,浏览器会自动保存文件。但是目前我的下载需求有点复杂,要先通过权限校验,再去执行下载操作。多了鉴权这一步,我们在提交下载请求的时候,可能要携带用户的cookie信息,而Download
这种方式的话,没办法携带cookie值,也没有办法通过权限校验。于是就探究了一下,前端文件下载的方式,做个总结。
特点:最简单、便捷的方式,点击一下到后端,后端返回数据浏览器就能自动保存文件
<a href='后端服务的链接' download="文件名">点击下载a>
特点:可以附加一些其他的功能响应,能做的操作更多。
<button type="button" onclick="downloadFile()">downloadbutton>
const downloadFile = () => {
window.location.href = '后端服务链接'; // 方式一
window.open('后端服务链接'); // 方式二
}
这种方式,等同于你在浏览器上直接输入【下载地址】,如果后端的响应头content-type与content-disposition设置不对的话,如果你下载的文件是【可执行文件】,比如说html,css,js这种,浏览器会打开这个文件,而不是保存文件。
除此之外,还存在一些问题:
window.location.href 如果下载多个文件,而你点击太快的时候,会导致请求重置,又重新下载一遍。
window.open()的这个效果,相信你肯定见过,浏览器会打开一个空白的页面,地址栏上链接,就是页面空白的,然后浏览器会保存文件。这对用户体验来说,并不友好。当然,你可能会说,那我再window.close不就行了吗?也不是不行,至少比那些留着个空白页面的要好点。
<form action="后端下载链接">
<input type='submit'>下载input>
form>
前面的两种方式都是比较基础的,入门级方式。只能适用于一些简单的应用,前端能够直接call到后端的,没什么复杂需求的,也没有使用框架的。那一般使用框架、需要请求头携带cookie来进行权限校验的复杂下载请求,该怎么去做呢?这种时候,一般会通过框架去请求后端,后端返回来的数据并不会直接触发浏览器保存文件,而是将数据返回到response中;然后再操作dom,在界面添加一些元素再去触发浏览器来保存文件。下面举的例子是基于axios来实现的下载请求,ajax或者其它工具也大同小异。
什么?iframe?这不是用来做内联框架的标签而已吗?这能够用来下载?你忘了iframe还有个src属性了吗,这个可以用来控制iframe显示的内容。但是我们的目的并不是想让他显示,只是想让它打开链接来触发浏览器保存文件。
<button type="button" onclick="downloadFile()">download</button>
const downloadFile = async () => {
// 注意,直接通过iframe的src去访问后端的话,是无法实现携带cookie去做权限校验的,在这里我的axios实现了携带cookie了的,所以我得先发送一次axios请求
let res = await axiosSend();
// 其实在上面那步执行返回,数据已经保存到了response中,但是无法触发浏览器保存文件
// 接下来我们操作dom来添加一个iframe
let iframe = document.createElement('iframe');
// 防止页面上弹出一个小框
iframe.style.display = 'none';
// axios的响应格式是这样可以获取url,其他的你可以根据具体的响应结果来修改这个src,这个src由于依赖了前面的请求,所以再次发送的时候,也会携带cookie值
iframe.src = res.request.responseURL;
// 在它加载的时候我们就remove了,不要了
iframe.onload = function () {
document.body.removeChild(iframe);
}
// 将这个iframe嵌入到页面元素中
document.body.appendChild(iframe);
}
通过上面的这种方式,就可以实现鉴权文件下载功能。你如果不需要权限校验,其实也可以直接通过iframe的src去访问后端下载链接,也是很不错的一个选择。它的优点也是很明显的,由于每次点击,促发下载事件,都是打开一个新的iframe去执行下载,所以不会出现前面的下载请求重置的问题,适合用于多文件下载。iframe没有展示请求路径,隐秘性较好。
除此之外,iframe还支持跨域,有兴趣的小伙伴可以自行了解一下。
缺点的话,也比较明显,每次下载都要去访问后端两次(如果你要鉴权的话),如果并发量很高的情况下,可能就问题很大了。
这个和前面那个form的原理其实是一样的,只不过也是通过操作dom来实现的,而且通过这种方式实现的也是可以支持下载多文件。
const downlaodFile = () => {
let form = document.createElement('form');
document.body.appendChild (form);
form.method = "GET";
form.action = '后端下载链接';
form.submit();
document.body.removeChild(form);
}
至于想实现鉴权下载,和前面的一样,都需要先通过axios去访问一遍,然后带着cookie值再去访问一遍后端。
可能这个你在看到上一个方法的form的时候已经想到了,那我嵌入一个a标签岂不是更直接吗?确实如此。这种方式也可以实现多文件下载。鉴权下载同上。
const downloadFile = () => {
let a = document.createElement('a');
a.style.display = 'none';
a.href = '后端下载链接';
a.download = '文件名称';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
看到这里,或许你感觉很嫌弃这些方法。想鉴权都必须要访问两次后端,那有没有更好的办法呢?你可能会想,我可不可以拿出来第一次访问后端返回的数据,然后再保存下来,这样不就可以只访问一次后端了嘛。
在这里就要讲一下Blob类,官方文档
Blob
对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream
来用于数据操作。
我们可以先将第一次axios访问后端返回的数据封装成一个blob对象,然后使用blob创建一个指向类型化数组的URL,再用上面的4、5、6任意一种方式去访问这个URL,也可以将文件保存,而且只是访问了一次后端。
const downloadFile = async () => {
let res = await axiosSend(); // 假设我的res就是返回的二进制数据
let blob = new Blob([res]); // 将二进制数据封装成blob对象
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = URL.createObjectURL(blob); // 创建指向blob数据的url
iframe.onload = function () {
document.body.removeChild(iframe);
}
document.body.appendChild(iframe);
URL.revokeObjectURL(iframe.src); // 释放URL对象
}
同样的,你可以用a标签、form表单的形式去实现同样的功能。
这里再贴上一个a标签的方式,因为这个会比较常用,方便我自己直接复制,哈哈
const downloadFile = async () => {
let res = await axiosSend(); // 假设我的res就是返回的二进制数据
let fileName = res.headers["content-disposition"].split(";")[1].split("=")[1].replace(/^"|"$/g, ''); // 获取文件名
let contentType = res.headers['content-type']; // 获取content-type
let blob = new Blob([res.data], { type: contentType }); // 将二进制数据封装成blob对象
let a = document.createElement('a');
a.style.display = 'none';
a.href = URL.createObjectURL(blob);
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href); // 释放URL对象
}