我有一个javascript应用程序,发送ajax POST请求到某个URL。Response可以是JSON字符串,也可以是文件(作为附件)。我可以很容易地在ajax调用中检测到Content-Type和Content-Disposition,但是一旦我检测到响应包含一个文件,我如何让客户端下载它呢?我在这里读过一些类似的帖子,但没有一个能提供我想要的答案。

请,请,请不要发布建议我不应该使用ajax或我应该重定向浏览器的答案,因为这些都不是一个选项。使用普通的HTML表单也是不可行的。我所需要的是向客户端显示一个下载对话框。这能做到吗?如何做到?


当前回答

不要这么快就放弃,因为这可以(在现代浏览器中)使用FileAPI的部分:

var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.onload = function () {
    if (this.status === 200) {
        var blob = this.response;
        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, '');
        }

        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.href = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location.href = downloadUrl;
            }

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

或者如果使用jQuery.ajax:

$.ajax({
    type: "POST",
    url: url,
    data: params,
    xhrFields: {
        responseType: 'blob' // to avoid binary data being mangled on charset conversion
    },
    success: function(blob, status, xhr) {
        // check for a filename
        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, '');
        }

        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.href = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location.href = downloadUrl;
            }

            setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
        }
    }
});

其他回答

正如其他人所述,您可以通过POST请求创建并提交表单来下载。但是,您不必手动执行此操作。

有一个非常简单的库可以做到这一点,那就是jquery.redirect。它提供了一个类似于标准jQuery的API。post方法:

$.redirect(url, [values, [method, [target]]])

对于那些寻找更现代的方法的人,您可以使用fetch API。下面的代码展示了如何下载电子表格文件。

fetch(url, {
    body: JSON.stringify(data),
    method: 'POST',
    headers: {
        'Content-Type': 'application/json; charset=utf-8'
    },
})
.then(response => response.blob())
.then(response => {
    const blob = new Blob([response], {type: 'application/application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
    const downloadUrl = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = downloadUrl;
    a.download = "file.xlsx";
    document.body.appendChild(a);
    a.click();
})

我相信这种方法比其他XMLHttpRequest解决方案更容易理解。此外,它具有与jQuery方法相似的语法,不需要添加任何额外的库。

当然,我建议检查一下你正在开发的浏览器,因为这种新方法在IE上行不通。您可以在以下[链接][1]上找到完整的浏览器兼容性列表。

重要提示:在本例中,我将发送一个JSON请求到侦听给定url的服务器。这个url必须设置,在我的例子中,我假设你知道这一部分。另外,考虑请求工作所需的头文件。因为我发送一个JSON,我必须添加内容类型头,并将其设置为application/ JSON;Charset =utf-8,以便让服务器知道它将接收的请求类型。

以下是我从不同渠道收集的解决方案: 服务器端实现:

    String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
    // Set headers
    response.setHeader("content-disposition", "attachment; filename =" + fileName);
    response.setContentType(contentType);
    // Copy file to output stream
    ServletOutputStream servletOutputStream = response.getOutputStream();
    try (InputStream inputStream = new FileInputStream(file)) {
        IOUtils.copy(inputStream, servletOutputStream);
    } finally {
        servletOutputStream.flush();
        Utils.closeQuitely(servletOutputStream);
        fileToDownload = null;
    }

客户端实现(使用jquery):

$.ajax({
type: 'POST',
contentType: 'application/json',
    url: <download file url>,
    data: JSON.stringify(postObject),
    error: function(XMLHttpRequest, textStatus, errorThrown) {
        alert(errorThrown);
    },
    success: function(message, textStatus, response) {
       var header = response.getResponseHeader('Content-Disposition');
       var fileName = header.split("=")[1];
       var blob = new Blob([message]);
       var link = document.createElement('a');
       link.href = window.URL.createObjectURL(blob);
       link.download = fileName;
       link.click();
    }
});   

我看到你已经找到了一个解决方案,但我只是想添加一些信息,这可能有助于有人试图实现与大POST请求相同的事情。

几周前我也遇到了同样的问题,确实不可能通过AJAX实现“干净”的下载,Filament Group创建了一个jQuery插件,它的工作方式与您已经发现的完全相同,它被称为jQuery文件下载,但这种技术有一个缺点。

If you're sending big requests through AJAX (say files +1MB) it will negatively impact responsiveness. In slow Internet connections you'll have to wait a lot until the request is sent and also wait for the file to download. It isn't like an instant "click" => "popup" => "download start". It's more like "click" => "wait until data is sent" => "wait for response" => "download start" which makes it appear the file double its size because you'll have to wait for the request to be sent through AJAX and get it back as a downloadable file.

如果您使用的是小于1MB的小文件,则不会注意到这一点。但正如我在自己的应用程序中发现的那样,对于更大的文件大小来说,这几乎是无法忍受的。

我的应用程序允许用户导出动态生成的图像,这些图像通过POST请求以base64格式发送到服务器(这是唯一可能的方式),然后处理并以.png, .jpg文件的形式发送回用户,图像的base64字符串+1MB是巨大的,这迫使用户等待超过必要的文件开始下载。在网速较慢的情况下,这真的很烦人。

我的解决方案是将文件临时写入服务器,一旦它准备好了,动态生成一个链接到文件的按钮形式,在“请等待…”和“下载”状态之间变化,同时,在预览弹出窗口中打印base64映像,以便用户可以“右键单击”并保存它。这使得所有的等待时间对用户来说更容易忍受,也加快了速度。

2014年9月30日更新:

自从我发布这篇文章以来,几个月过去了,最后我找到了一个更好的方法来加快使用大base64字符串的速度。我现在存储base64字符串到数据库(使用longtext或longblog字段),然后我通过jQuery文件下载它的记录ID,最后在下载脚本文件上,我查询数据库使用这个ID拉base64字符串并通过下载函数传递它。

下载脚本示例:

<?php
// Record ID
$downloadID = (int)$_POST['id'];
// Query Data (this example uses CodeIgniter)
$data       = $CI->MyQueries->GetDownload( $downloadID );
// base64 tags are replaced by [removed], so we strip them out
$base64     = base64_decode( preg_replace('#\[removed\]#', '', $data[0]->image) );
// This example is for base64 images
$imgsize    = getimagesize( $base64 );
// Set content headers
header('Content-Disposition: attachment; filename="my-file.png"');
header('Content-type: '.$imgsize['mime']);
// Force download
echo $base64;
?>

我知道这远远超出了OP的要求,但是我觉得用我的发现更新我的答案会很好。当我在寻找我的问题的解决方案时,我读了很多“从AJAX POST数据下载”的帖子,这些帖子并没有给我我想要的答案,我希望这些信息能帮助那些想要实现这样的事情的人。

我使用这个FileSaver.js。在我的情况下,与csv文件,我这样做(在coffescript):

  $.ajax
    url: "url-to-server"
    data: "data-to-send"
    success: (csvData)->
      blob = new Blob([csvData], { type: 'text/csv' })
      saveAs(blob, "filename.csv")

我认为对于大多数复杂的情况,必须对数据进行正确的处理。在底层,filesver .js实现了与Jonathan Amend的答案相同的方法。