背景
某日接到用户反馈,说某个模块下载文件失败,查看用户截图后确认问题如下:
- 系统:windows
- 浏览器:搜狗(发现只有该浏览器有问题 )
- 下载框中看到,浏览器没有正确识别文件名称及后缀,而是把接口路径的最后一段作为文件名,导致无法打开文件
经过排查发现,与 a 标签的 download 属性的设置有关,这就引出了问题,下载文件是如何实现的,下载文件名是如何确定的,于是有了这篇总结,下面开始进入正题 (●゚ω゚●)
业务代码中前端下载文件的逻辑是参考 file-saver 实现的
function downloadFile(url: string, fileName: string = '') {
const a = document.createElement('a');
a.download = fileName;
a.rel = 'noopener'; // tabnabbing, 防止打开的窗口获取操作源窗口的 window 对象
a.href = url;
a.target = '_blank';
// `a.click()` doesn't work for all browsers (#465)
try {
a.dispatchEvent(new MouseEvent('click'));
} catch (e) {
const evt = document.createEvent('MouseEvents');
evt.initMouseEvent(
'click',
true,
true,
window,
0,
0,
0,
80,
20,
false,
false,
false,
false,
0,
null
);
a.dispatchEvent(evt);
}
}
后端下载文件接口的响应头如下:
Content-Disposition: attachment; filename*=UTF-8''%E6%B5%8B%E8%AF%95.xlsx
Content-Type: application/octet-stream
<a> download vs Content-Disposition
<a> download 属性
<a>: The Anchor elementdeveloper.mozilla.org功能
下载目标链接而不是导航到目标链接
使用场景
对于浏览器可识别的文件格式,如图片,txt,pdf 等,使用 download 属性可实现文件下载,否则会在浏览器窗口中打开文件预览
使用方式
<a href="xxx" download>xxx</> // 表示超链接用于下载而不是跳转
<a href="xxx" download="xxx.xx">xxx</> // 指定下载文件的名字
使用限制
- 同域链接
- data:URL
- blob:URL
download 属性为空时,filename 的确定
- filename: Content-Disposition 的 filename 字段 / The final segment in the URL path
- extension: Content-Type / start of the data:URL / Blob.type for the blob:URL
注:当 Content-Disposition 的 filename 与 download 值不同时,filename 的优先级更高The attribute can furthermore be given a value, to specify the file name that user agents are to use when storing the resource in a file system. This value can be overridden by the `Content-Disposition` HTTP header's filename parameters.
Content-Disposition
作为 HTTP 响应头时,语法如下:
Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"; filename*=ext-value
Disposition Type
- inline: 表示响应作为网页或网页的一部分在浏览器中展示
- attachment: 表示响应作为附件下载并保存到本地
Filename
- filename
- 支持 ISO-8859-1 字符集
- 作为保存文件时呈现给用户的文件名
- 当 Disposition Type 为 inline 时,作为用户后续保存文件时的文件名称(如右键另存为的情况)
- filename*
- 与 filename 的区别是,filename * 支持 ISO-8859-1 字符集之外的字符
- 按照 RFC 5987 中的编码方式编码
- 当与 filename 同时存在时,filename* 的优先级更高
ISO-8859-1 是 ASCII 字符集的扩展,加入了 96 个字母及符号,以供使用附加符号的拉丁字母语言使用
Filename 编码问题
最初 http 头的值只支持 ASCII 码,直到 RFC 5987 提出了统一的编码方式,使用扩展参数:
parameter*=charset'lang'value
其中:
- charset 必填,ASCII 字符,不区分大小写
- lang 选填,ASCII 字符,不区分大小写
- value 原始名称经过指定编码后再经过百分号编码的值
- 三者以
'
分隔 - 浏览器应至少支持 ASCII 编码和 utf-8 编码
- 当参数值格式错误或无法解析是,会忽略该参数
- 出于安全考虑,如果 filename 的值带有
/
或会被转成
_
的形式
小结
- download 属性一般用于纯前端的下载
- Content-Disposition 用于后端下载。在我们的业务中,几乎都是这种情况,download 属性实际上并没有起到作用,而且它还对某个浏览器有兼容性问题,所以我们的解决方案就是去掉 download 属性设置。
新的疑问
不知道当你看完上面的内容,心中是不是得到了一个结论,让后端在 HTTP 请求结果中设置正确的 Content-Disposition 头就可以实现下载了?请接着往下看...
我们知道常见的下载方式有:
- 上文提到的 downloadFile 的方式,即动态创建 a 标签,模拟点击的方式进行下载
- window.open() 的方式
- form 表单提交的方式
有想过为什么这些方式可以吗?
回答:
文件下载其实是浏览器的行为,我们必须要用一个浏览器上下文去承载响应。这就可以理解上面几种方式了。a 标签点击,window.open 都会打开一个页面。
form 提交呢?
我们想想表单提交之后返回了啥?它有一个 target 的属性,表示在提交表单后,在哪里展示响应信息,可以设置 _self, _blank, _parent, _top
,默认值是 _self
,表示在相同的浏览器上下文中加载响应。那就可以理解 form 提交为啥能支持下载文件了。
栗子
可在下面的代码仓库中查看相关栗子,加深理解二者对文件下载以及文件名称的影响。
https://github.com/superdc/download-file-demogithub.com