JavaScript/jQuery 通过 POST 使用 JSON 数据下载文件
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 
原文地址: http://stackoverflow.com/questions/3499597/
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
JavaScript/jQuery to download file via POST with JSON data
提问by Tauren
I have a jquery-based single-page webapp. It communicates with a RESTful web service via AJAX calls.
我有一个基于 jquery 的单页 web 应用程序。它通过 AJAX 调用与 RESTful Web 服务进行通信。
I'm trying to accomplish the following:
我正在尝试完成以下工作:
- Submit a POST that contains JSON data to a REST url.
- If the request specifies a JSON response, then JSON is returned.
- If the request specifies a PDF/XLS/etc response, then a downloadable binary is returned.
- 将包含 JSON 数据的 POST 提交到 REST url。
- 如果请求指定 JSON 响应,则返回 JSON。
- 如果请求指定 PDF/XLS/etc 响应,则返回可下载的二进制文件。
I have 1 & 2 working now, and the client jquery app displays the returned data in the web page by creating DOM elements based on the JSON data. I also have #3 working from the web-service point of view, meaning it will create and return a binary file if given the correct JSON parameters. But I'm unsure the best way to deal with #3 in the client javascript code.
我现在有 1 和 2 工作,客户端 jquery 应用程序通过基于 JSON 数据创建 DOM 元素在网页中显示返回的数据。从 Web 服务的角度来看,我还有 #3 工作,这意味着如果给定正确的 JSON 参数,它将创建并返回一个二进制文件。但我不确定在客户端 javascript 代码中处理 #3 的最佳方法。
Is it possible to get a downloadable file back from an ajax call like this? How do I get the browser to download and save the file?
是否有可能从这样的 ajax 调用中获取可下载的文件?如何让浏览器下载并保存文件?
$.ajax({
    type: "POST",
    url: "/services/test",
    contentType: "application/json",
    data: JSON.stringify({category: 42, sort: 3, type: "pdf"}),
    dataType: "json",
    success: function(json, status){
        if (status != "success") {
            log("Error loading data");
            return;
        }
        log("Data loaded!");
    },
    error: function(result, status, err) {
        log("Error loading data");
        return;
    }
});
The server responds with the following headers:
服务器使用以下标头进行响应:
Content-Disposition:attachment; filename=export-1282022272283.pdf
Content-Length:5120
Content-Type:application/pdf
Server:Jetty(6.1.11)
Another idea is to generate the PDF and store it on the server and return JSON that includes a URL to the file. Then, issue another call in the ajax success handler to do something like the following:
另一个想法是生成 PDF 并将其存储在服务器上,然后返回包含文件 URL 的 JSON。然后,在 ajax 成功处理程序中发出另一个调用以执行如下操作:
success: function(json,status) {
    window.location.href = json.url;
}
But doing that means I would need to make more than one call to the server, and my server would need to build downloadable files, store them somewhere, then periodically clean up that storage area.
但是这样做意味着我需要多次调用服务器,并且我的服务器需要构建可下载的文件,将它们存储在某处,然后定期清理该存储区域。
There must be a simpler way to accomplish this. Ideas?
必须有一种更简单的方法来实现这一点。想法?
EDIT: After reviewing the docs for $.ajax, I see that the response dataType can only be one of xml, html, script, json, jsonp, text, so I'm guessing there is no way to directly download a file using an ajax request, unless I embed the binary file in using Data URI scheme as suggested in the @VinayC answer (which is not something I want to do).
编辑:在查看 $.ajax 的文档后,我看到响应 dataType 只能是其中之一xml, html, script, json, jsonp, text,所以我猜没有办法使用 ajax 请求直接下载文件,除非我嵌入二进制文件使用@VinayC 答案中建议的数据 URI 方案(这不是我想做的事情)。
So I guess my options are:
所以我想我的选择是:
- Not use ajax and instead submit a form post and embed my JSON data into the form values. Would probably need to mess with hidden iframes and such. 
- Not use ajax and instead convert my JSON data into a query string to build a standard GET request and set window.location.href to this URL. May need to use event.preventDefault() in my click handler to keep browser from changing from the application URL. 
- Use my other idea above, but enhanced with suggestions from the @naikus answer. Submit AJAX request with some parameter that lets web-service know this is being called via an ajax call. If the web service is called from an ajax call, simply return JSON with a URL to the generated resource. If the resource is called directly, then return the actual binary file. 
- 不使用 ajax,而是提交表单帖子并将我的 JSON 数据嵌入到表单值中。可能需要弄乱隐藏的 iframe 等。 
- 不使用 ajax,而是将我的 JSON 数据转换为查询字符串以构建标准 GET 请求并将 window.location.href 设置为此 URL。可能需要在我的点击处理程序中使用 event.preventDefault() 以防止浏览器从应用程序 URL 更改。 
- 使用我上面的其他想法,但通过@naikus 答案中的建议进行了增强。使用一些参数提交 AJAX 请求,让 Web 服务知道这是通过 ajax 调用调用的。如果 Web 服务是从 ajax 调用中调用的,只需将带有 URL 的 JSON 返回到生成的资源。如果直接调用资源,则返回实际的二进制文件。 
The more I think about it, the more I like the last option. This way I can get information back about the request (time to generate, size of file, error messages, etc.) and I can act on that information before starting the download. The downside is extra file management on the server.
我想得越多,我就越喜欢最后一个选项。通过这种方式,我可以获取有关请求的信息(生成时间、文件大小、错误消息等),并且可以在开始下载之前根据该信息采取行动。缺点是服务器上的额外文件管理。
Any other ways to accomplish this? Any pros/cons to these methods I should be aware of?
还有其他方法可以实现吗?我应该知道这些方法的任何优点/缺点?
回答by SamStephens
letronje's solution only works for very simple pages. document.body.innerHTML +=takes the HTML text of the body, appends the iframe HTML, and sets the innerHTML of the page to that string. This will wipe out any event bindings your page has, amongst other things. Create an element and use appendChildinstead.
letronje的解决方案仅适用于非常简单的页面。document.body.innerHTML +=获取正文的 HTML 文本,附加 iframe HTML,并将页面的 innerHTML 设置为该字符串。这将消除您的页面具有的任何事件绑定等。创建一个元素并使用appendChild。
$.post('/create_binary_file.php', postData, function(retData) {
  var iframe = document.createElement("iframe");
  iframe.setAttribute("src", retData.url);
  iframe.setAttribute("style", "display: none");
  document.body.appendChild(iframe);
}); 
Or using jQuery
或者使用 jQuery
$.post('/create_binary_file.php', postData, function(retData) {
  $("body").append("<iframe src='" + retData.url+ "' style='display: none;' ></iframe>");
}); 
What this actually does: perform a post to /create_binary_file.php with the data in the variable postData; if that post completes successfully, add a new iframe to the body of the page. The assumption is that the response from /create_binary_file.php will include a value 'url', which is the URL that the generated PDF/XLS/etc file can be downloaded from. Adding an iframe to the page that references that URL will result in the browser promoting the user to download the file, assuming that the web server has the appropriate mime type configuration.
这实际上是做什么的:使用变量 postData 中的数据对 /create_binary_file.php 执行发布;如果该帖子成功完成,请在页面正文中添加一个新的 iframe。假设来自 /create_binary_file.php 的响应将包含一个值“url”,即可以从中下载生成的 PDF/XLS/etc 文件的 URL。向引用该 URL 的页面添加 iframe 将导致浏览器促使用户下载文件,假设 Web 服务器具有适当的 mime 类型配置。
回答by JoshBerke
I've been playing around with another option that uses blobs. I've managed to get it to download text documents, and I've downloaded PDF's (However they are corrupted).
我一直在玩另一个使用 blob 的选项。我已经设法让它下载文本文档,并且我已经下载了 PDF(但是它们已损坏)。
Using the blob API you will be able to do the following:
使用 blob API,您将能够执行以下操作:
$.post(/*...*/,function (result)
{
    var blob=new Blob([result]);
    var link=document.createElement('a');
    link.href=window.URL.createObjectURL(blob);
    link.download="myFileName.txt";
    link.click();
});
This is IE 10+, Chrome 8+, FF 4+. See https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL
这是 IE 10+、Chrome 8+、FF 4+。见https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL
It will only download the file in Chrome, Firefox and Opera. This uses a download attribute on the anchor tag to force the browser to download it.
它只会在 Chrome、Firefox 和 Opera 中下载文件。这使用锚标记上的下载属性来强制浏览器下载它。
回答by amersk
I know this kind of old, but I think I have come up with a more elegant solution. I had the exact same problem. The issue I was having with the solutions suggested were that they all required the file being saved on the server, but I did not want to save the files on the server, because it introduced other problems (security: the file could then be accessed by non-authenticated users, cleanup: how and when do you get rid of the files). And like you, my data was complex, nested JSON objects that would be hard to put into a form.
我知道这种老方法,但我想我想出了一个更优雅的解决方案。我有同样的问题。我在建议的解决方案中遇到的问题是,它们都要求将文件保存在服务器上,但我不想将文件保存在服务器上,因为它引入了其他问题(安全性:文件可以通过未经身份验证的用户,清理:您如何以及何时删除文件)。和您一样,我的数据是复杂的嵌套 JSON 对象,很难放入表单中。
What I did was create two server functions. The first validated the data. If there was an error, it would be returned. If it was not an error, I returned all of the parameters serialized/encoded as a base64 string. Then, on the client, I have a form that has only one hidden input and posts to a second server function. I set the hidden input to the base64 string and submit the format. The second server function decodes/deserializes the parameters and generates the file. The form could submit to a new window or an iframe on the page and the file will open up.
我所做的是创建两个服务器功能。第一个验证了数据。如果有错误,它将被返回。如果不是错误,我将所有序列化/编码为 base64 字符串的参数返回。然后,在客户端上,我有一个只有一个隐藏输入并发布到第二个服务器功能的表单。我将隐藏输入设置为 base64 字符串并提交格式。第二个服务器功能解码/反序列化参数并生成文件。表单可以提交到页面上的新窗口或 iframe,文件将打开。
There's a little bit more work involved, and perhaps a little bit more processing, but overall, I felt much better with this solution.
需要做更多的工作,也许还需要更多的处理,但总的来说,我对这个解决方案感觉好多了。
Code is in C#/MVC
代码在 C#/MVC 中
    public JsonResult Validate(int reportId, string format, ReportParamModel[] parameters)
    {
        // TODO: do validation
        if (valid)
        {
            GenerateParams generateParams = new GenerateParams(reportId, format, parameters);
            string data = new EntityBase64Converter<GenerateParams>().ToBase64(generateParams);
            return Json(new { State = "Success", Data = data });
        }
        return Json(new { State = "Error", Data = "Error message" });
    }
    public ActionResult Generate(string data)
    {
        GenerateParams generateParams = new EntityBase64Converter<GenerateParams>().ToEntity(data);
        // TODO: Generate file
        return File(bytes, mimeType);
    }
on the client
在客户端
    function generate(reportId, format, parameters)
    {
        var data = {
            reportId: reportId,
            format: format,
            params: params
        };
        $.ajax(
        {
            url: "/Validate",
            type: 'POST',
            data: JSON.stringify(data),
            dataType: 'json',
            contentType: 'application/json; charset=utf-8',
            success: generateComplete
        });
    }
    function generateComplete(result)
    {
        if (result.State == "Success")
        {
            // this could/should already be set in the HTML
            formGenerate.action = "/Generate";
            formGenerate.target = iframeFile;
            hidData = result.Data;
            formGenerate.submit();
        }
        else
            // TODO: display error messages
    }
回答by aqm
There is a simplier way, create a form and post it, this runs the risk of resetting the page if the return mime type is something that a browser would open, but for csv and such it's perfect
有一种更简单的方法,创建一个表单并发布它,如果返回的 mime 类型是浏览器会打开的东西,这会冒着重置页面的风险,但对于 csv 等,它是完美的
Example requires underscore and jquery
示例需要下划线和 jquery
var postData = {
    filename:filename,
    filecontent:filecontent
};
var fakeFormHtmlFragment = "<form style='display: none;' method='POST' action='"+SAVEAS_PHP_MODE_URL+"'>";
_.each(postData, function(postValue, postKey){
    var escapedKey = postKey.replace("\", "\\").replace("'", "\'");
    var escapedValue = postValue.replace("\", "\\").replace("'", "\'");
    fakeFormHtmlFragment += "<input type='hidden' name='"+escapedKey+"' value='"+escapedValue+"'>";
});
fakeFormHtmlFragment += "</form>";
$fakeFormDom = $(fakeFormHtmlFragment);
$("body").append($fakeFormDom);
$fakeFormDom.submit();
For things like html, text and such, make sure the mimetype is some thing like application/octet-stream
对于 html、text 等内容,请确保 mimetype 类似于 application/octet-stream
php code
代码
<?php
/**
 * get HTTP POST variable which is a string ?foo=bar
 * @param string $param
 * @param bool $required
 * @return string
 */
function getHTTPPostString ($param, $required = false) {
    if(!isset($_POST[$param])) {
        if($required) {
            echo "required POST param '$param' missing";
            exit 1;
        } else {
            return "";
        }
    }
    return trim($_POST[$param]);
}
$filename = getHTTPPostString("filename", true);
$filecontent = getHTTPPostString("filecontent", true);
header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"$filename\"");
echo $filecontent;
回答by Frank Rem
It is been a while since this question was asked but I had the same challenge and want to share my solution. It uses elements from the other answers but I wasn't able to find it in its entirety. It doesn't use a form or an iframe but it does require a post/get request pair. Instead of saving the file between the requests, it saves the post data. It seems to be both simple and effective.
自从提出这个问题已经有一段时间了,但我遇到了同样的挑战并想分享我的解决方案。它使用了其他答案中的元素,但我无法完整地找到它。它不使用表单或 iframe,但它确实需要一个 post/get 请求对。它不是在请求之间保存文件,而是保存发布数据。这似乎既简单又有效。
client
客户
var apples = new Array(); 
// construct data - replace with your own
$.ajax({
   type: "POST",
   url: '/Home/Download',
   data: JSON.stringify(apples),
   contentType: "application/json",
   dataType: "text",
   success: function (data) {
      var url = '/Home/Download?id=' + data;
      window.location = url;
   });
});
server
服务器
[HttpPost]
// called first
public ActionResult Download(Apple[] apples)
{
   string json = new JavaScriptSerializer().Serialize(apples);
   string id = Guid.NewGuid().ToString();
   string path = Server.MapPath(string.Format("~/temp/{0}.json", id));
   System.IO.File.WriteAllText(path, json);
   return Content(id);
}
// called next
public ActionResult Download(string id)
{
   string path = Server.MapPath(string.Format("~/temp/{0}.json", id));
   string json = System.IO.File.ReadAllText(path);
   System.IO.File.Delete(path);
   Apple[] apples = new JavaScriptSerializer().Deserialize<Apple[]>(json);
   // work with apples to build your file in memory
   byte[] file = createPdf(apples); 
   Response.AddHeader("Content-Disposition", "attachment; filename=juicy.pdf");
   return File(file, "application/pdf");
}
回答by VinayC
In short, there is no simpler way. You need to make another server request to show PDF file. Al though, there are few alternatives but they are not perfect and won't work on all browsers:
简而言之,没有更简单的方法。您需要发出另一个服务器请求以显示 PDF 文件。尽管如此,几乎没有替代方案,但它们并不完美,并且不适用于所有浏览器:
- Look at data URI scheme. If binary data is small then you can perhaps use javascript to open window passing data in URI.
- Windows/IE only solution would be to have .NET control or FileSystemObject to save the data on local file system and open it from there.
- 查看数据 URI 方案。如果二进制数据很小,那么您也许可以使用 javascript 打开窗口传递 URI 中的数据。
- Windows/IE 唯一的解决方案是让 .NET 控件或 FileSystemObject 将数据保存在本地文件系统上并从那里打开它。
回答by James McGuigan
$scope.downloadSearchAsCSV = function(httpOptions) {
  var httpOptions = _.extend({
    method: 'POST',
    url:    '',
    data:   null
  }, httpOptions);
  $http(httpOptions).then(function(response) {
    if( response.status >= 400 ) {
      alert(response.status + " - Server Error \nUnable to download CSV from POST\n" + JSON.stringify(httpOptions.data));
    } else {
      $scope.downloadResponseAsCSVFile(response)
    }
  })
};
/**
 * @source: https://github.com/asafdav/ng-csv/blob/master/src/ng-csv/directives/ng-csv.js
 * @param response
 */
$scope.downloadResponseAsCSVFile = function(response) {
  var charset = "utf-8";
  var filename = "search_results.csv";
  var blob = new Blob([response.data], {
    type: "text/csv;charset="+ charset + ";"
  });
  if (window.navigator.msSaveOrOpenBlob) {
    navigator.msSaveBlob(blob, filename); // @untested
  } else {
    var downloadContainer = angular.element('<div data-tap-disabled="true"><a></a></div>');
    var downloadLink      = angular.element(downloadContainer.children()[0]);
    downloadLink.attr('href', window.URL.createObjectURL(blob));
    downloadLink.attr('download', "search_results.csv");
    downloadLink.attr('target', '_blank');
    $document.find('body').append(downloadContainer);
    $timeout(function() {
      downloadLink[0].click();
      downloadLink.remove();
    }, null);
  }
  //// Gets blocked by Chrome popup-blocker
  //var csv_window = window.open("","","");
  //csv_window.document.write('<meta name="content-type" content="text/csv">');
  //csv_window.document.write('<meta name="content-disposition" content="attachment;  filename=data.csv">  ');
  //csv_window.document.write(response.data);
};
回答by ralftar
Not entirely an answer to the original post, but a quick and dirty solution for posting a json-object to the server and dynamically generating a download.
不完全是对原始帖子的回答,而是将 json 对象发布到服务器并动态生成下载的快速而肮脏的解决方案。
Client side jQuery:
客户端jQuery:
var download = function(resource, payload) {
     $("#downloadFormPoster").remove();
     $("<div id='downloadFormPoster' style='display: none;'><iframe name='downloadFormPosterIframe'></iframe></div>").appendTo('body');
     $("<form action='" + resource + "' target='downloadFormPosterIframe' method='post'>" +
      "<input type='hidden' name='jsonstring' value='" + JSON.stringify(payload) + "'/>" +
      "</form>")
      .appendTo("#downloadFormPoster")
      .submit();
}
..and then decoding the json-string at the serverside and setting headers for download (PHP example):
..然后在服务器端解码json字符串并设置下载头(PHP示例):
$request = json_decode($_POST['jsonstring']), true);
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename=export.csv');
header('Pragma: no-cache');
回答by naikus
I think the best approach is to use a combination, Your second approach seems to be an elegant solution where browsers are involved.
我认为最好的方法是使用组合,您的第二种方法似乎是涉及浏览器的优雅解决方案。
So depending on the how the call is made. (whether its a browser or a web service call) you can use a combination of the two, with sending a URL to the browser and sending raw data to any other web service client.
因此,取决于呼叫的方式。(无论是浏览器还是 Web 服务调用)您可以结合使用两者,将 URL 发送到浏览器并将原始数据发送到任何其他 Web 服务客户端。
回答by Otis-iDev
I have been awake for two days now trying to figure out how to download a file using jquery with ajax call. All the support i got could not help my situation until i try this.
我已经醒了两天,现在试图弄清楚如何使用带有 ajax 调用的 jquery 下载文件。在我尝试这个之前,我得到的所有支持都无法帮助我的处境。
Client Side
客户端
function exportStaffCSV(t) {
   
    var postData = { checkOne: t };
    $.ajax({
        type: "POST",
        url: "/Admin/Staff/exportStaffAsCSV",
        data: postData,
        success: function (data) {
            SuccessMessage("file download will start in few second..");
            var url = '/Admin/Staff/DownloadCSV?data=' + data;
            window.location = url;
        },
       
        traditional: true,
        error: function (xhr, status, p3, p4) {
            var err = "Error " + " " + status + " " + p3 + " " + p4;
            if (xhr.responseText && xhr.responseText[0] == "{")
                err = JSON.parse(xhr.responseText).Message;
            ErrorMessage(err);
        }
    });
}Server Side
服务器端
 [HttpPost]
    public string exportStaffAsCSV(IEnumerable<string> checkOne)
    {
        StringWriter sw = new StringWriter();
        try
        {
            var data = _db.staffInfoes.Where(t => checkOne.Contains(t.staffID)).ToList();
            sw.WriteLine("\"First Name\",\"Last Name\",\"Other Name\",\"Phone Number\",\"Email Address\",\"Contact Address\",\"Date of Joining\"");
            foreach (var item in data)
            {
                sw.WriteLine(string.Format("\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\",\"{5}\",\"{6}\"",
                    item.firstName,
                    item.lastName,
                    item.otherName,
                    item.phone,
                    item.email,
                    item.contact_Address,
                    item.doj
                    ));
            }
        }
        catch (Exception e)
        {
        }
        return sw.ToString();
    }
    //On ajax success request, it will be redirected to this method as a Get verb request with the returned date(string)
    public FileContentResult DownloadCSV(string data)
    {
        return File(new System.Text.UTF8Encoding().GetBytes(data), System.Net.Mime.MediaTypeNames.Application.Octet, filename);
        //this method will now return the file for download or open.
    }
Good luck.
祝你好运。

