前端下载文件遇到的一些问题
在前段时间的开发中,有一个下载分片视频的需求,在做这个需求的过程中遇到了几个坑,这里来记录一下。
在这个需求中,同一个视频被分成了一到多个视频片段,如果有一个视频片段在点击下载按钮时直接下载;如果有多个视频片段则需要选择后点击一次下载按钮时完成对所有视频片段的下载,即不能将视频的下载链接写成 <a href="xx" download>video href</a> 这种形式逐个点击后下载。接下来,我们就来看一看前端中到底有多少种方式可以用来下载文件。
前端下载文件的方式
1、使用 a 标签下载
通常情况下我们使用 a 标签进行页面之间的跳转,但是 a 标签还有一个隐藏功能,那就是 文件下载。默认情况下,如果 a 标签的 href 属性的值是一个指向浏览器可以打开的 MIME 中的一种时,浏览器会加载该 URI 指向的文件的并展示出来;如果 URI 指向的文件并不能被浏览器展现时,则会被下载到本地。
而在 HTML5 中,a 标签新增来一个 download 属性,如果一个 a 标签在使用时添加了 download 属性的话,在点击时,浏览器会将 href 指向的文件下载到本地。如果 download 属性设置了值的话,该属性的值会作为下载到本地文件的名字。
但是,如果 a 标签的 href 是指向的一个接口,通过接口下载文件的话,download 属性即使设置了值,也不能更改下载到本地的文件的名字;同样,下载 OSS 上的文件,也不能通过设置 download 属性来改变下载到本地文件的名字。所以,如果使用 a 标签下载文件并且想修改下载到本地的文件名时,需要服务端配合修改 HTTP 的协议头 Content-Disposition。
2、使用虚拟 a 标签下载
由于产品形态或者其他原因,有时候产品不允许在页面中出现 实体的 a 标签,这里所谓的实体的 a 标签是指真实存在于 dom 树中的 a 标签,这种时候可以借助存在于内存中的 虚拟 a 标签 来实现下载的功能。
在现代浏览器中,绝大部份浏览器都支持在内存中创建 HTML 标签,并可以在虚拟标签上触发该标签所允许的事件,例如 click 等事件。
使用虚拟 a 标签实现下载功能的代码如下:
function download () {
let a = document.createElement('a')
a.href = 'uri/to/file'
a.download = 'filename'
// a.style.display = 'none' # 设置 a 标签不可见
// document.body.append(a) # 添加到 dom 树中
a.click()
// document.body.remove(a) # 从 dom 树中移除
}
download()
上面的代码现在内存中创建了一个 a 标签,设置 href 和 download 属性之后,添加到 dom 树中;然后通过 a.click() 触发 a 标签的点击事件,由于 a 标签设置了 download 属性,所以浏览器会解析 URI 并将文件下载到本地;最后通过 HTML 向外暴露的 remove 方法将 a 标签从 dom 树中移除。
其实即使不将 a 标签添加到 dom 树中,也可以通过 a.click() 触发 a 标签的点击事件实现下载;所以,在使用的过程中,可以省略上面代码中被注释的部分,少改变一次 dom 树,毕竟任何的操作都是有消耗的。
3、使用 window.open 方法下载
同没有添加 download 属性的 a 标签一样,可以通过 window.open 方法下载部分文件,这些可以下载的文件是不能被浏览器展现出来的文件;对于可以被浏览器解析并展现的文件,windown.open 方法只会在新打开的窗口渲染文件内容,并不会下载到本地。
除了以上的问题外,使用 window.open 还会出现以下几个问题:
window.open方法还会先打开一个空白的页面,然后在新打开的页面中实现下载,体验不是很好;- 新打开的页面不会自己关闭,需要开发者自己手动关闭新打开的页面,这里就会出现一个问题:如果关闭新窗口的代码执行的太早,下载的请求链接没有传输完成时,则该下载会被中断。而且开发者没有办法知道下载请求链接是否完成,所以要么不关闭新打开的窗口,由用户关闭;或者设置一个比较大的定时器,由定时器来关闭新打开的空白页面。
- 对于异步获取的下载 url,通过
window.open打开新页面时会被浏览器拦截,即该页面不会被打开,会被浏览器折叠在地址栏的最右边,需要用户手动信任后才能下载;
所以不建议通过 window.open 方法下载文件。
**注:**对于使用 window.open 打开异步获取的 url 被浏览器拦截的问题,可以通过先创建新的空白页面,然后设置 url 的方式打开:
let w = window.open()
let url = (async function () {
return await getUrlAsync()
})()
w.location = url
另:不要通过 window.open 方法打开不安全的下载页面,因为新打开的页面可以通过 window.opener 获取你的页面引用。详见 rel=noopener。
4、使用 iframe 下载
使用 iframe 下载文件与使用 虚拟 a 标签下载 或者 使用 window.open 方法下载 具有一样的局限:只能下载浏览器不能渲染的文件。其本质也是借助浏览器会下载不能渲染的文件的特性。
下载代码与使用 虚拟 a 标签下载 差不多:
let f = document.createElement('iframe')
# document.body.append(f)
f.src = 'URL/to/file'
# document.body.remove(f)
注释部分的代码可以不用,因为可以通过内存中的 iframe 实现下载。
5、使用 blob + ObjectURL + a 标签的方式下载
该实现方式的代码如下:
/**
* 获取 blob
* @param {String} url 目标文件地址
* @return {Promise}
*/
function getBlob(url) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response);
}
};
xhr.send();
});
}
/**
* 保存
* @param {Blob} blob
* @param {String} filename 想要保存的文件名称
*/
function saveAs(blob, filename) {
if (window.navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, filename);
} else {
const link = document.createElement('a');
const body = document.querySelector('body');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
// fix Firefox
link.style.display = 'none';
body.appendChild(link);
link.click();
body.removeChild(link);
window.URL.revokeObjectURL(link.href);
}
}
/**
* 下载
* @param {String} url 目标文件地址
* @param {String} filename 想要保存的文件名称
*/
function download(url, filename) {
getBlob(url).then(blob => {
saveAs(blob, filename);
});
}
注:©上面的代码摘抄自 JavaScript 实现文件下载并重命名。
这种方式的下载原理:
- 1、通过 Ajax 请求将要下载的文件以
blob的格式下载到本地; - 2、通过
window.URL.createObjectURL(blob)创建一个标识文件对象的 Object URL;更多文件操作的内容可以参考 前端 file api; - 3、通过 使用虚拟 a 标签下载 下载到本地;
使用该方式具有以下几种限制:
- 1、需要下载 blob 格式的文件,所以需要服务器支持
responseType: blob; - 2、需要先将文件下载到本地之后再使用
window.URL.createObjectURL(blob)创建 Object URL,所以如果文件比较大,ajax 请求需要很久才能下载完成,下载期间没有任何反应,所以体验不好;
优势:
- 可以本地控制修改文件名,即使是下载 OSS 文件也可以在本地定义文件名。
服务端通过修改 HTTP 协议头实现修改文件名
上面的下载方式都不能很好的解决本地下载且修改文件名的问题;这里要讲的下载方式可以比较好的解决问题:既可以持续下载让用户看到,又能本地修改下载后的文件名字。
我们知道服务器上的静态文件可以通过 a 标签 + download 属性的方式实现下载,并且可以修改下载到本地的文件名字;而 OSS 上的文件,或者通过请求接口下载的文件,不能通过设置 download 属性来修改下载到本地的文件的名字,这个时候可以请服务端配合,在下载接口中返回如下 HTTP 协议头:
'Content-Disposition: attachment; filename="downloaded.pdf"'
浏览器在请求响应时,如发现该 HTTP 协议头,会将 filename 的值设置为下载文件的名字,这样就可以避免使用 blob 方式下载时的“假死”问题,也修改了下载文件的名字。
可以在设计接口时,留一个设置文件名的参数,这样就可以在调用下载接口时,将想要设置的文件名以参数的形式传递到服务端;服务端接口在响应时,在响应中带上 HTTP 协议头,通知浏览器修改下载文件的名字。
