关于javascript:从ajax帖子处理文件下载

Handle file download from ajax post

我有一个JavaScript应用程序,它将Ajax Post请求发送到特定的URL。响应可以是一个JSON字符串,也可以是一个文件(作为附件)。在Ajax调用中,我可以很容易地检测到内容类型和内容部署,但是一旦检测到响应包含一个文件,如何向客户机提供下载它的服务?我在这里读过许多类似的主题,但没有一个提供我要寻找的答案。

拜托,拜托,拜托,请不要发表我不应该使用Ajax的答案,或者我应该重定向浏览器,因为这都不是一个选项。使用纯HTML表单也不是一个选项。我需要的是向客户机显示一个下载对话框。这能做到吗?如何做到?

编辑:

显然,这是不可能的,但是有一个简单的解决方法,正如被接受的答案所建议的那样。对于将来遇到这个问题的任何人,我都会这样解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$.ajax({
    type:"POST",
    url: url,
    data: params,
    success: function(response, status, request) {
        var disp = request.getResponseHeader('Content-Disposition');
        if (disp && disp.search('attachment') != -1) {
            var form = $('<form method="POST" action="' + url + '">');
            $.each(params, function(k, v) {
                form.append($('<input type="hidden" name="' + k +
                        '" value="' + v + '">'));
            });
            $('body').append(form);
            form.submit();
        }
    }
});

因此,基本上,只需生成一个HTML表单,其中包含与Ajax请求中使用的相同参数,然后提交它。


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

编辑2017-09-28:更新为在可用时使用文件构造函数,使其在Safari中工作>=10.1。

编辑2015-10-16:jQueryAjax无法正确处理二进制响应(无法设置ResponseType),因此最好使用普通的xmlhttpRequest调用。

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
47
48
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[^;=
]*=((['"]).*?\2|[^;
]*)/;
            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));

这里是使用jquery.ajax的旧版本。当响应转换为某个字符集的字符串时,它可能会损坏二进制数据。

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
$.ajax({
    type:"POST",
    url: url,
    data: params,
    success: function(response, status, xhr) {
        // check for a filename
        var filename ="";
        var disposition = xhr.getResponseHeader('Content-Disposition');
        if (disposition && disposition.indexOf('attachment') !== -1) {
            var filenameRegex = /filename[^;=
]*=((['"]).*?\2|[^;
]*)/;
            var matches = filenameRegex.exec(disposition);
            if (matches != null && matches[1]) filename = matches[1].replace(/['
"]/g, '');
        }

        var type = xhr.getResponseHeader('Content-Type');
        var blob = new Blob([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
        }
    }
});


创建一个表单,使用post方法,提交表单-不需要iframe。当服务器页面响应请求时,为文件的mime类型编写一个响应头,它将显示一个下载对话框-我已经做了很多次了。

您需要应用程序/下载的内容类型-只需搜索如何为您使用的任何语言提供下载。


我也面临同样的问题,并成功地解决了它。我的用例是这样的。

"将JSON数据发布到服务器并接收Excel文件。该Excel文件由服务器创建,并作为对客户端的响应返回。在浏览器中将该响应下载为具有自定义名称的文件"

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
$("#my-button").on("click", function(){

// Data to post
data = {
    ids: [1, 2, 3, 4, 5]
};

// Use XMLHttpRequest instead of Jquery $ajax
xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
    var a;
    if (xhttp.readyState === 4 && xhttp.status === 200) {
        // Trick for making downloadable link
        a = document.createElement('a');
        a.href = window.URL.createObjectURL(xhttp.response);
        // Give filename you wish to download
        a.download ="test-file.xls";
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
    }
};
// Post data to URL which handles post request
xhttp.open("POST", excelDownloadUrl);
xhttp.setRequestHeader("Content-Type","application/json");
// You should set responseType as blob for binary responses
xhttp.responseType = 'blob';
xhttp.send(JSON.stringify(data));
});

上面的代码片段只是在执行以下操作

  • 使用xmlhttpRequest将数组作为json发布到服务器。
  • 在以blob(二进制)形式获取内容之后,我们创建了一个可下载的URL,并将其附加到不可见的"a"链接上,然后单击它。

这里我们需要在服务器端小心地设置一些东西。我在python django httpresponse中设置了一些头文件。如果使用其他编程语言,则需要相应地设置它们。

1
2
# In python django code
response = HttpResponse(file_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")

因为我在这里下载了XLS(Excel),所以我将ContentType调整为上面的一个。您需要根据您的文件类型设置它。您可以使用此技术下载任何类型的文件。


您使用的是什么服务器端语言?在我的应用程序中,通过在PHP响应中设置正确的头,我可以轻松地从Ajax调用中下载文件:

正在设置服务器端的邮件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
header("HTTP/1.1 200 OK");
header("Pragma: public");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");

// The optional second 'replace' parameter indicates whether the header
// should replace a previous similar header, or add a second header of
// the same type. By default it will replace, but if you pass in FALSE
// as the second argument you can force multiple headers of the same type.
header("Cache-Control: private", false);

header("Content-type:" . $mimeType);

// $strFileName is, of course, the filename of the file being downloaded.
// This won't have to be the same name as the actual file.
header("Content-Disposition: attachment; filename="{$strFileName}"");

header("Content-Transfer-Encoding: binary");
header("Content-Length:" . mb_strlen($strFile));

// $strFile is a binary representation of the file that is being downloaded.
echo $strFile;

这实际上会将浏览器"重定向"到这个下载页面,但正如@ahren alread在他的评论中所说,它不会离开当前页面。

这一切都是关于设置正确的头文件,所以我相信您会找到一个适合您使用的服务器端语言的解决方案,如果它不是PHP的话。

处理响应客户端

假设您已经知道如何进行Ajax调用,那么在客户机端,您将向服务器执行Ajax请求。然后,服务器会生成一个链接,从中可以下载此文件,例如,要指向的"转发"URL。例如,服务器响应为:

1
2
3
4
5
{
    status: 1, // ok
    // unique one-time download token, not required of course
    message: 'http://yourwebsite.com/getdownload/ska08912dsa'
}

在处理响应时,在您的身体中注入一个iframe,并将iframe的SRC设置为您刚刚收到的这样的URL(使用jquery以方便本例的使用):

1
2
$("body").append("<iframe src='" + data.message +
 "' style='display: none;' ></iframe>");

如果您设置了如上所示的正确标题,那么iframe将强制下载对话框,而无需将浏览器从当前页面移开。

注释

与您的问题相关的额外功能;我认为在使用Ajax技术请求东西时,最好总是返回JSON。收到JSON响应之后,您就可以决定客户端如何处理它了。例如,稍后您可能希望用户单击指向URL的下载链接,而不是直接强制下载,在当前设置中,您必须同时更新客户端和服务器端才能执行此操作。


对于那些从角度寻找解决方案的人来说,这对我很有用:

1
2
3
4
5
6
7
8
9
10
11
12
$http.post(
  'url',
  {},
  {responseType: 'arraybuffer'}
).then(function (response) {
  var headers = response.headers();
  var blob = new Blob([response.data],{type:headers['content-type']});
  var link = document.createElement('a');
  link.href = window.URL.createObjectURL(blob);
  link.download ="Filename";
  link.click();
});


这是我如何使这个工作https://stackoverflow.com/a/27563953/2845977

1
2
3
4
5
6
7
8
9
10
$.ajax({
  url: '<URL_TO_FILE>',
  success: function(data) {
    var blob=new Blob([data]);
    var link=document.createElement('a');
    link.href=window.URL.createObjectURL(blob);
    link.download="<FILENAME_TO_SAVE_WITH_EXTENSION>";
    link.click();
  }
});

使用download.js更新了答案

1
2
3
4
$.ajax({
  url: '<URL_TO_FILE>',
  success: download.bind(true,"<FILENAME_TO_SAVE_WITH_EXTENSION>","<FILE_MIME_TYPE>")
});


我知道你已经找到了一个解决方案,但是我只是想添加一些信息,这些信息可能会帮助一些人通过大量的发帖请求来实现相同的目标。

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

如果您通过Ajax发送大的请求(比如文件+1MB),它将对响应产生负面影响。在缓慢的互联网连接中,您需要等待很多时间,直到请求被发送,也要等待文件下载。它不像一个即时的"点击"=>"弹出"=>"下载开始"。它更像是"click"=>"wait until data is sent"=>"wait for response"=>"download start",这使得文件的大小增加了一倍,因为您需要等待通过Ajax发送请求,并将其作为可下载文件返回。

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

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

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

2014年9月30日更新:

Months have passed since I posted this, finally I've found a better approach to speed things up when working with big base64 strings. I now store base64 strings into the database (using longtext or longblog fields), then I pass its record ID through the jQuery File Download, finally on the download script file I query the database using this ID to pull the base64 string and pass it through the download function.

下载脚本示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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 Data下载"线程,这些线程没有给出我所寻找的答案,我希望这些信息能够帮助那些寻求类似解决方案的人。


我想指出在接受的答案中使用该技术时出现的一些困难,即使用表格邮寄:

  • 您不能对请求设置邮件头。如果您的身份验证模式涉及头,即在授权头中传递的JSON Web令牌,那么您必须找到其他方法来发送它,例如作为查询参数。

  • 您不能真正知道请求何时完成。好吧,您可以使用一个在响应时设置好的cookie,就像jquery.filedownload所做的那样,但它远不是完美的。它不适用于并发请求,如果响应从未到达,它将中断。

  • 如果服务器响应错误,用户将被重定向到错误页。

  • 只能使用表单支持的内容类型。这意味着您不能使用JSON。

  • 我最终使用的方法是将文件保存在S3上,并发送一个预先签名的URL来获取文件。


    这是我使用临时隐藏表单的解决方案。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //Create an hidden form
    var form = $('<form>', {'method': 'POST', 'action': this.href}).hide();

    //Add params
    var params = { ...your params... };
    $.each(params, function (k, v) {
        form.append($('<input>', {'type': 'hidden', 'name': k, 'value': v}));
    });

    //Make it part of the document and submit
    $('body').append(form);
    form.submit();

    //Clean up
    form.remove();

    请注意,我大量使用jquery,但您也可以使用本机JS。


    这是一个3年前的问题,但我今天也遇到了同样的问题。我看过你编辑过的解决方案,但我认为它会牺牲性能,因为它必须提出双重请求。因此,如果有人需要另一个解决方案,这并不意味着要两次调用服务,那么我就是这样做的:

    1
    2
    3
    <form id="export-csv-form" method="POST" action="/the/path/to/file">
        <input type="hidden" name="anyValueToPassTheServer" value="">
    </form>

    此窗体仅用于调用服务并避免使用window.location()。之后,您只需从jquery提交表单,就可以调用服务并获取文件。这很简单,但是这样你就可以用一篇文章来下载了。我现在认为,如果您所呼叫的服务是GeT,这会更容易,但我不是这样。


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

    jquery.redirect是一个非常简单的库,可以精确地完成这项工作。它提供了一个类似于标准jQuery.post方法的API:

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


    为了让乔纳森对《边缘》中的工作作出回应,我做了以下更改:

    1
    2
    3
    var blob = typeof File === 'function'
        ? new File([this.response], filename, { type: type })
        : new Blob([this.response], { type: type });

    对此

    1
    2
    3
    4
    var f = typeof File+"";
    var blob = f === 'function' && Modernizr.fileapi
        ? new File([this.response], filename, { type: type })
        : new Blob([this.response], { type: type });

    我宁愿把这个作为评论发表,但我没有足够的声誉


    参见:http://www.henryalgus.com/reading-binary-files-using-jquery-ajax/它将返回一个blob作为响应,然后将其放入filesaver中。


    我使用了这个filesaver.js。对于csv文件,我这样做(在coffescript中):

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

    我认为对于最复杂的情况,必须正确处理数据。在hood filesaver.js下,实现与jonathan amend的答案相同的方法。


    以下是我的解决方案,来自不同的来源:服务器端实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        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):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $.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();
        }
    });