Ajax 请求后台接口【下载文件】方法

你的管理台有一个功能是点击[查看文件],下载文件到本地的功能。你可能自然的向后台接口(可能是 Nodejs 的一个 Restful Api)请求下载。但是你发现你后台的 Nodejs 虽然设置了正确的响应头(Content-position, Content-type)却还是没有弹出保存文件的框。

原因是未经过处理的 ajax(如jquery ajax请求) 请求不能处理正确的二进制(流)类型的文件。

下面我总结了几种方式可以解决此问题-> 📚🌲


利用iframe

Nodejs 端正常的设置相应头,Content-disposition: attachment, filename=。然后读到服务器上对应的文件赋值给 ctx.body 即可。下面是一种参考的代码:

1
2
3
4
5
6
7
8
const filePath = path.join(path.resolve(__dirname, '../uploads'), ${fileStamp})

ctx.set({
'Content-disposition': `attachment; filename=${encodeURIComponent(fileStamp)}`,
'Content-type': 'application/octet-stream'
})

ctx.body = fs.createReadStream(filePath)

前端利用 iframe 的 src 属性,进行重新赋值。注意 iframe 设为 display: none 属性。

1
2
3
4
5
6
<iframe ref="downloadIframe" src="" frameborder="0" style="display: none;"></iframe>

// 点击查看时执行 downloadFile
downloadFile(fileStamp) {
this.$refs.downloadIframe.src = `/download/${fileStamp}`
}

利用URL.createObjectURL

URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。

一个 Blob对象表示一个不可变的, 原始数据的类似文件对象。Blob表示的数据不一定是一个JavaScript原生格式。 File 接口基于Blob,继承 blob功能并将其扩展为支持用户系统上的文件。(HTML5新增特性)

这种方法我已经在上篇文章刚好有用到,可以利用浏览器原生 fetch 方法,然后解析为 blob 类型。再利用a标签的 download 属性即可。参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function fetchApi (url, data) {
return fetch(url, {
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
cache: 'no-cache',
method: 'POST',
mode: 'cors'
})
}

// 注意这里要是 response.blob()
function getImageData(url, data) {
return fetchApi(url, data)
.then(response => response.blob())
}

function getImage(data) {
getImageData('/downloadImage', data)
.then(response => {
let a = document.createElement('a')
let url = window.URL.createObjectURL(response)
let filename = `${data.scene}-${+new Date()}.png`

a.href = url
a.download = filename

a.click()
window.URL.revokeObjectURL(url)
})
}

当然你可以不用 fetch 的这个方法,直接在 xhr 里进行封装。我看到 stackoverflow 上有个很好的文章,地址戳这里,我觉的最高回答者已经解释的很好了,下面引用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (this.status === 200) {
var filename = "";
var disposition = xhr.getResponseHeader('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
}
var type = xhr.getResponseHeader('Content-Type');

var blob = typeof File === 'function'
? new File([this.response], filename, { type: type })
: new Blob([this.response], { type: type });
if (typeof window.navigator.msSaveBlob !== 'undefined') {
// IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
window.navigator.msSaveBlob(blob, filename);
} else {
var URL = window.URL || window.webkitURL;
var downloadUrl = URL.createObjectURL(blob);

if (filename) {
// use HTML5 a[download] attribute to specify filename
var a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location = downloadUrl;
} else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}
} else {
window.location = downloadUrl;
}

setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
}
}
};
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send($.param(params));

大概的思路是利用 xhr 原生对象的 onload 后,进行new blob()对象。

再执行URL.createObjectURL(blob)的操作。


总结

其实还有别的方法,如 POST 一个请求,但是这个会刷新当前的 Url。上面两种情况其实是两种思路。

POST 请求跟 Iframe 类似,同样还可以 window.open 或者 window.location 来做这件事情。