jQuery 处理从 ajax 帖子下载的文件

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16086162/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-26 16:23:18  来源:igfitidea点击:

Handle file download from ajax post

javascriptjqueryajax

提问by Pavle Predic

I have a javascript app that sends ajax POST requests to a certain URL. Response might be a JSON string or it might be a file (as an attachment). I can easily detect Content-Type and Content-Disposition in my ajax call, but once I detect that the response contains a file, how do I offer the client to download it? I've read a number of similar threads here but none of them provide the answer I'm looking for.

我有一个 javascript 应用程序,可以将 ajax POST 请求发送到某个 URL。响应可能是一个 JSON 字符串,也可能是一个文件(作为附件)。我可以在 ajax 调用中轻松检测 Content-Type 和 Content-Disposition,但是一旦检测到响应包含文件,我该如何提供客户端下载它?我在这里阅读了许多类似的主题,但没有一个提供我正在寻找的答案。

Please, please, please do not post answers suggesting that I shouldn't use ajax for this or that I should redirect the browser, because none of this is an option. Using a plain HTML form is also not an option. What I do need is to show a download dialog to the client. Can this be done and how?

拜托,拜托,请不要发布暗示我不应该为此使用ajax或应该重定向浏览器的答案,因为这些都不是一个选项。使用纯 HTML 表单也不是一种选择。我需要的是向客户端显示下载对话框。这可以做到吗?如何做到?

采纳答案by Pavle Predic

Create a form, use the POST method, submit the form - there's no need for an iframe. When the server page responds to the request, write a response header for the mime type of the file, and it will present a download dialog - I've done this a number of times.

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

You want content-type of application/download - just search for how to provide a download for whatever language you're using.

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

回答by Jonathan Amend

Don't give up so quickly, because this can be done (in modern browsers) using parts of the FileAPI:

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

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]*=((['"]).*?|[^;\n]*)/;
            var matches = filenameRegex.exec(disposition);
            if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
        }
        var type = xhr.getResponseHeader('Content-Type');

        var blob;
        if (typeof File === 'function') {
            try {
                blob = new File([this.response], filename, { type: type });
            } catch (e) { /* Edge */ }
        }
        if (typeof blob === 'undefined') {
            blob = 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.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));

Here is the old version using jQuery.ajax. It might mangle binary data when the response is converted to a string of some charset.

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

$.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[^;=\n]*=((['"]).*?|[^;\n]*)/;
            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.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
        }
    }
});

回答by Naren Yellavula

I faced the same issue and successfully solved it. My use-case is this.

我遇到了同样的问题并成功解决了它。我的用例是这样的。

"Post JSON data to the server and receive an excel file. That excel file is created by the server and returned as a response to the client. Download that response as a file with custom name in browser"

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

$("#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));
});

The above snippet is just doing following

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

  • Posting an array as JSON to the server using XMLHttpRequest.
  • After fetching content as a blob(binary), we are creating a downloadable URL and attaching it to invisible "a" link then clicking it.
  • 使用 XMLHttpRequest 将数组作为 JSON 发布到服务器。
  • 在将内容作为 blob(二进制)获取后,我们正在创建一个可下载的 URL 并将其附加到不可见的“a”链接,然后单击它。

Here we need to carefully set few things at the server side. I set few headers in Python Django HttpResponse. You need to set them accordingly if you use other programming languages.

这里我们需要在服务器端仔细设置一些东西。我在 Python Django HttpResponse 中设置了几个标头。如果您使用其他编程语言,则需要相应地设置它们。

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

Since I download xls(excel) here, I adjusted contentType to above one. You need to set it according to your file type. You can use this technique to download any kind of files.

由于我在这里下载了 xls(excel),因此我将 contentType 调整为高于 1。您需要根据您的文件类型进行设置。您可以使用此技术下载任何类型的文件。

回答by Robin van Baalen

What server-side language are you using? In my app I can easily download a file from an AJAX call by setting the correct headers in PHP's response:

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

Setting headers server-side

设置标题服务器端

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;

This will in fact 'redirect' the browser to this download page, but as @ahren alread said in his comment, it won't navigate away from the current page.

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

It's all about setting the correct headers so I'm sure you'll find a suitable solution for the server-side language you're using if it's not PHP.

这完全是关于设置正确的标头,所以我相信如果它不是 PHP,你会为你正在使用的服务器端语言找到合适的解决方案。

Handling the response client side

处理响应客户端

Assuming you already know how to make an AJAX call, on the client side you execute an AJAX request to the server. The server then generates a link from where this file can be downloaded, e.g. the 'forward' URL where you want to point to. For example, the server responds with:

假设您已经知道如何进行 AJAX 调用,则在客户端向服务器执行 AJAX 请求。然后服务器会生成一个链接,从那里可以下载该文件,例如您要指向的“转发”URL。例如,服务器响应:

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

When processing the response, you inject an iframein your body and set the iframe's SRC to the URL you just received like this (using jQuery for the ease of this example):

在处理响应时,你iframe在你的 body 中注入 an并将iframe的 SRC设置为你刚刚收到的 URL(为了简化这个例子,使用 jQuery):

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

If you've set the correct headers as shown above, the iframe will force a download dialog without navigating the browser away from the current page.

如果您设置了如上所示的正确标题,iframe 将强制下载对话框,而无需将浏览器导航到当前页面之外。

Note

笔记

Extra addition in relation to your question; I think it's best to always return JSON when requesting stuff with AJAX technology. After you've received the JSON response, you can then decide client-side what to do with it. Maybe, for example, later on you want the user to click a download link to the URL instead of forcing the download directly, in your current setup you would have to update both client and server-side to do so.

关于您的问题的额外补充;我认为最好在使用 AJAX 技术请求内容时始终返回 JSON。收到 JSON 响应后,您可以决定客户端如何处理它。例如,稍后您可能希望用户单击指向 URL 的下载链接,而不是直接强制下载,在您当前的设置中,您必须同时更新客户端和服务器端才能这样做。

回答by Tim Hettler

For those looking for a solution from an Angular perspective, this worked for me:

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

$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();
});

回答by Mayur Padshala

Here is how I got this working https://stackoverflow.com/a/27563953/2845977

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

$.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();
  }
});

Updated answer using download.js

使用download.js更新答案

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

回答by José SAYAGO

I see you've already found out a solution, however I just wanted to add some information which may help someone trying to achieve the same thing with big POST requests.

我看到您已经找到了一个解决方案,但是我只是想添加一些信息,这些信息可能会帮助试图通过大型 POST 请求实现相同目标的人。

I had the same issue a couple of weeks ago, indeed it isn't possible to achieve a "clean" download through AJAX, the Filament Group created a jQuery plugin which works exactly how you've already found out, it is called jQuery File Downloadhowever there is a downside to this technique.

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

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 lotuntil 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.

如果您通过 AJAX 发送大请求(比如文件 +1MB),它将对响应能力产生负面影响。在 Internet 连接速度较慢的情况下,您必须等待很长时间才能发送请求并等待文件下载。它不像即时“点击”=>“弹出”=>“下载开始”。这更像是“点击”=>“等待数据发送”=>“等待响应”=>“下载开始”这使得文件大小增加一倍,因为您必须等待发送请求通过 AJAX 并将其作为可下载文件取回。

If you're working with small file sizes <1MB you won't notice this. But as I discovered in my own app, for bigger file sizes it is almost unbearable.

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

My app allow users to export images dynamically generated, these images are sent through POST requests in base64 format to the server (it is the only possible way), then processed and sent back to users in form of .png, .jpg files, base64 strings for images +1MB are huge, this force users to wait more than necessary for the file to start downloading. In slow Internet connections it can be really annoying.

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

My solution for this was to temporary write the file to the server, once it is ready, dynamically generate a link to the file in form of a button which changes between "Please wait..." and "Download" states and at the same time, print the base64 image in a preview popup window so users can "right-click" and save it. This makes all the waiting time more bearable for users, and also speed things up.

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

Update Sep 30, 2014:

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.

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

Download Script Example:

下载脚本示例:

<?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;
?>

I know this is way beyond what the OP asked, however I felt it would be good to update my answer with my findings. When I was searching for solutions to my problem, I read lots of "Download from AJAX POST data"threads which didn't give me the answer I was looking for, I hope this information helps someone looking to achieve something like this.

我知道这远远超出了 OP 的要求,但是我觉得用我的发现更新我的答案会很好。当我为我的问题寻找解决方案时,我阅读了很多“从 AJAX POST 数据下载”线程,这些线程没有给我我正在寻找的答案,我希望这些信息可以帮助那些希望实现类似目标的人。

回答by tepez

I want to point out some difficulties that arise when using the technique in the accepted answer, i.e. using a form post:

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

  1. You can't set headers on the request. If your authentication schema involves headers, a Json-Web-Token passed in the Authorization header, you'll have to find other way to send it, for example as a query parameter.

  2. You can't really tell when the request has finished. Well, you can use a cookie that gets set on response, as done by jquery.fileDownload, but it's FAR from perfect. It won't work for concurrent requests and it will break if a response never arrives.

  3. If the server responds with a error, the user will be redirected to the error page.

  4. You can only use the content types supported by a form. Which means you can't use JSON.

  1. 您不能在请求上设置标头。如果您的身份验证架构涉及标头,即在 Authorization 标头中传递的 Json-Web-Token,则您必须找到其他方式来发送它,例如作为查询参数。

  2. 您无法真正判断请求何时完成。好吧,您可以使用在响应时设置的 cookie,就像jquery.fileDownload所做的那样,但它离完美还差得很远。它不适用于并发请求,如果响应永远不会到达,它将中断。

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

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

I ended up using the method of saving the file on S3 and sending a pre-signed URL to get the file.

我最终使用了将文件保存在 S3 上并发送预签名 URL 来获取文件的方法。

回答by Ludovic Martin

Here is my solution using a temporary hidden form.

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

//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();

Note that I massively use JQuery but you can do the same with native JS.

请注意,我大量使用 JQuery,但您可以使用本机 JS 做同样的事情。

回答by Alain Cruz

For those looking a more modern approach, you can use the fetch API. The following example shows how to download a spreadsheet file. It is easily done with the following code.

对于那些寻求更现代方法的人,您可以使用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();
})

I believe this approach to be much easier to understand than other XMLHttpRequestsolutions. Also, it has a similar syntax to the jQueryapproach, without the need to add any additional libraries.

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

Of course, I would advise checking to which browser you are developing, since this new approach won't work on IE. You can find the full browser compatibility list on the following [link][1].

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

Important: In this example I am sending a JSON request to a server listening on the given url. This urlmust be set, on my example I am assuming you know this part. Also, consider the headers needed for your request to work. Since I am sending a JSON, I must add the Content-Typeheader and set it to application/json; charset=utf-8, as to let the server know the type of request it will receive.

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