使用XMLHttpRequest提示文件下载

43

我知道jQuery的ajax方法无法处理下载,而且我不想添加一个jQuery插件来实现这个功能。

我想知道如何使用XMLHttpRequest发送POST数据来下载文件。

以下是我尝试过的内容:

var postData = new FormData();
postData.append('cells', JSON.stringify(output));

var xhr = new XMLHttpRequest();
xhr.open('POST', '/export/', true);
xhr.setRequestHeader("X-CSRFToken", csrftoken);
xhr.responseType = 'arraybuffer';
xhr.onload = function (e) {
    console.log(e);
    console.log(xhr);
}
xhr.send(postData);

我正在使用Django,该文件似乎已成功发送回客户端。在Chrome的网络选项卡中,我可以在预览选项卡中看到乱码(这是我预期的)。但是我想发送一个zip文件,而不是zip文件的文本表示。以下是Django后端代码:

wrapper = FileWrapper(tmp_file)
response = HttpResponse(wrapper, content_type='application/zip')
response['Content-Disposition'] = "attachment; filename=export.zip"
response['Content-Length'] = tmp_file.tell()
return response

我已经搜索了好几个小时,但是没有找到使用XMLHttpRequest的正确示例。我不想创建一个带有POST操作的真正html表单,因为表单数据相当大且是动态创建的。

上面的代码有什么问题吗?我漏掉了什么吗?我不知道如何将数据实际发送给客户端以下载。


我在这里没有看到任何问题。如果您将视图更改为在响应GET请求时返回文件,并在浏览器中打开URL,它是否按预期下载文件? - Marat
是的,它下载了@Marat。我能够通过使用普通的html表单并将其操作设为“/export/”来使其下载,并从xhr得到响应,但它只是不能触发下载。可能xhr不能将下载发送给客户端吗? - bozdoz
1
你能否取消接受我的答案,改为接受Steven的答案?让过时的答案排在前面真的很令人困惑。 - Marat
4个回答

60

如果在发送请求之前将XMLHttpRequest.responseType属性设置为'blob',那么当您收到响应时,它将表示为Blob对象。然后,您可以将该Blob对象保存到临时文件并导航到该文件。

var postData = new FormData();
postData.append('cells', JSON.stringify(output));

var xhr = new XMLHttpRequest();
xhr.open('POST', '/export/', true);
xhr.setRequestHeader('X-CSRFToken', csrftoken);
xhr.responseType = 'blob';
xhr.onload = function (e) {
    var blob = e.currentTarget.response;
    var contentDispo = e.currentTarget.getResponseHeader('Content-Disposition');
    // https://dev59.com/B2Ag5IYBdhLWcg3w5eZk#23054920/
    var fileName = contentDispo.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)[1];
    saveOrOpenBlob(blob, fileName);
}
xhr.send(postData);

以下是一个saveOrOpenBlob的实现示例:

function saveOrOpenBlob(blob, fileName) {
    window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
    window.requestFileSystem(window.TEMPORARY, 1024 * 1024, function (fs) {
        fs.root.getFile(fileName, { create: true }, function (fileEntry) {
            fileEntry.createWriter(function (fileWriter) {
                fileWriter.addEventListener("writeend", function () {
                    window.location = fileEntry.toURL();
                }, false);
                fileWriter.write(blob, "_blank");
            }, function () { });
        }, function () { });
    }, function () { });
}

如果您不介意在可查看的文件类型时,浏览器导航到该文件,则创建一个始终直接保存到文件的方法要简单得多:

function saveBlob(blob, fileName) {
    var a = document.createElement('a');
    a.href = window.URL.createObjectURL(blob);
    a.download = fileName;
    a.dispatchEvent(new MouseEvent('click'));
}

13
这是一种适用于小文件的好方法,但请注意,在将文件发送到磁盘之前,它会将整个文件放入内存,因此如果要下载非常大的文件,请考虑这一点。此外,用户无法看到实际下载的进度(例如在Chrome状态栏中),他们所看到的是从内存到磁盘的“下载”进度(一旦实际下载完成)。 - Daniel
在将您的解决方案集成到我的项目中后,我冒昧地修复了您最优秀答案中的一些问题。如果我引入了任何错误,请告诉我。 - Arnaud P
1
似乎“match”返回了一个带有下划线的文件名(当content-disposition文件名包含引号时)。 - sookie
@sookie你可以在saveBlob函数内使用fileName = fileName.slice(1, -1);来避免下划线。 - the_ccalderon
2
请注意,saveOrOpenBlob函数使用requestFileSystem,这在MDN上不被推荐:https://developer.mozilla.org/en-US/docs/Web/API/Window/requestFileSystem - HappyGoLucky
显示剩余2条评论

22

更新:自从Blob API的引入,这个回答已经不再准确。请参考Steven的回答获取详细信息。


原始回答:

XHR请求不会触发文件下载。我找不到明确的要求,但是W3C关于XMLHttpRequest的文档也没有描述content-disposition=attachment响应的任何特殊反应。

如果它不是POST请求,您可以通过window.open()在单独的选项卡中下载文件。在这里建议使用带有target=_blank的隐藏表单。


不确定您是否真的需要window.open。我已经完成了我想做的事情,没有使用target="_blank"。但我认为这是正确的,因为XHR不会触发下载。 - bozdoz
如果他正在使用POST请求,但之后需要下载怎么办? - gumuruh
1
@gumuruh 我不确定与原始问题有什么区别。另外,请注意这个答案现在已经不再是事实(请参见更新部分)。 - Marat
在新标签页中打开会导致客户端下载文件两次(除非缓存),这对于大文件或移动连接是需要考虑的。一次是为了检查是否下载,另一次是在新标签页中加载时下载。 - Matthias
这里有一个非常有趣的技巧,效果相当不错:点击此处 - James Toomey

4

download: function(){
    var postData = new FormData();
  var xhr = new XMLHttpRequest();
  xhr.open('GET', downloadUrl, true);
  xhr.responseType = 'blob';
  xhr.onload = function (e) {
   var blob = xhr.response;
   this.saveOrOpenBlob(blob);
  }.bind(this)
  xhr.send(postData);
 }

saveOrOpenBlob: function(blob) {
  var assetRecord = this.getAssetRecord();
  var fileName = 'Test.mp4'
  var tempEl = document.createElement("a");
     document.body.appendChild(tempEl);
     tempEl.style = "display: none";
        url = window.URL.createObjectURL(blob);
        tempEl.href = url;
        tempEl.download = fileName;
        tempEl.click();
  window.URL.revokeObjectURL(url);
 },

你可以试试这个,它对我有效。


你可能不需要将tempEl附加到document.body上,但这应该可以正常工作。我也喜欢revokeObjectURL - bozdoz

0

对我来说,使用fetch API可以工作。在React/Typescript项目中,我无法让XMLHttpRequest正常工作。也许我没有尝试足够多。不管怎样,问题已经解决了。这是代码:

const handleDownload = () => {
    const url = `${config.API_ROOT}/download_route/${entityId}`;
    const headers = new Headers();
    headers.append('Authorization', `Token ${token}`);

    fetch(url, { headers })
        .then((response) => response.blob())
        .then((blob) => {
            // Create  blob  URL
            const blobUrl = window.URL.createObjectURL(blob);

            // Create a temporary anchor el
            const anchorEl = document.createElement('a');
            anchorEl.href = blobUrl;
            anchorEl.download = `case_${entityId}_history.pdf`; // Set any filename and extension
            anchorEl.style.display = 'none';

            // Append the a tag to the DOM and click it to trigger download
            document.body.appendChild(anchorEl);
            anchorEl.click();

            // Clean up
            document.body.removeChild(anchorEl);
            window.URL.revokeObjectURL(blobUrl);
        })
        .catch((error) => {
            console.error('Error downloading file:', error);
        });
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接