JavaScript / jQuery通过POST和JSON数据下载文件

我有一个基于jQuery的单页web应用程序。 它通过AJAX调用与RESTful Web服务进行通信。

我试图完成以下内容:

  1. 将包含JSON数据的POST提交给REST URL。
  2. 如果请求指定了JSON响应,则返回JSON。
  3. 如果请求指定了PDF / XLS / etc响应,则返回可下载的二进制文件。

我现在有1和2工作,客户端jQuery应用程序通过创build基于JSON数据的DOM元素在网页中显示返回的数据。 从Web服务的angular度来看,我也有#3的工作,这意味着如果给出正确的JSON参数,它将创build并返回一个二进制文件。 但我不确定在客户端JavaScript代码中处理#3的最佳方法。

是否有可能从这样的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; } }); 

服务器使用以下标题进行响应:

 Content-Disposition:attachment; filename=export-1282022272283.pdf Content-Length:5120 Content-Type:application/pdf Server:Jetty(6.1.11) 

另一个想法是生成PDF并将其存储在服务器上,并返回包含该文件URL的JSON。 然后,在ajax成功处理程序中发出另一个调用,执行以下操作:

 success: function(json,status) { window.location.href = json.url; } 

但是这样做意味着我需要对服务器进行多个调用,而我的服务器需要构build可下载的文件,将它们存储在某个地方,然后定期清理该存储区域。

必须有一个更简单的方法来完成这个。 想法?


编辑:审查$ .ajax的文档后,我看到响应dataType只能是xml, html, script, json, jsonp, text ,所以我猜没有办法直接下载文件使用ajax请求,除非我使用@VinayC答案(这不是我想要做的)中build议的使用Data URIschemeembedded二进制文件。

所以我想我的select是:

  1. 不使用ajax,而是提交表单post,并将我的JSON数据embedded到表单值中。 可能需要弄乱隐藏的iframe等。

  2. 不使用ajax,而是将我的JSON数据转换为查询string来构build标准的GET请求,并将window.location.href设置为此URL。 可能需要在我的点击处理程序中使用event.preventDefault(),以防止浏览器从应用程序URL更改。

  3. 使用我上面的其他想法,但增强@naikus答案的build议。 用一些参数提交AJAX请求,让web服务知道这是通过ajax调用。 如果Web服务是从ajax调用中调用的,只需将JSON与URL一起返回到生成的资源。 如果直接调用资源,则返回实际的二进制文件。

我越想越想越喜欢最后的select。 这样我就可以获得有关请求的信息(生成时间,文件大小,错误消息等),我可以在开始下载之前根据这些信息采取行动。 缺点是服务器上的额外文件pipe理。

任何其他方式来完成这个? 这些方法的优点/缺点我应该知道?

letronje的解决scheme只适用于非常简单的页面。 document.body.innerHTML +=获取正文的HTML文本,附加iframe HTML,并将页面的innerHTML设置为该string。 这将消除您的页面中包含的任何事件绑定。 创build一个元素,然后使用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); }); 

或者使用jQuery

 $.post('/create_binary_file.php', postData, function(retData) { $("body").append("<iframe src='" + retData.url+ "' style='display: none;' ></iframe>"); }); 

这实际上是这样做的:使用variablespostData中的数据对/create_binary_file.php执行一个后置处理; 如果该post成功完成,请将新的iframe添加到页面正文中。 假定来自/create_binary_file.php的响应将包含一个值'url',它是生成的PDF / XLS / etc文件可以从中下载的URL。 假设Web服务器具有适当的MIMEtypesconfiguration,向引用该URL的页面添加iframe将导致浏览器促使用户下载该文件。

我一直在玩另一个使用blob的选项。 我已经设法让它下载文本文件,我已经下载了PDF文件(但是他们已经损坏)。

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

这是IE 10+,Chrome 8 +,FF 4+。 请参阅https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL

它只会在Chrome,Firefox和Opera下载文件。 这会在锚标签上使用下载属性来强制浏览器下载它。

我知道这种旧的,但我想我已经想出了一个更优雅的解决scheme。 我有完全相同的问题。 我提出的解决scheme的问题是,他们都需要文件被保存在服务器上,但我不想保存在服务器上的文件,因为它引入了其他问题(安全性:该文件可以通过访问未经身份validation的用户,清理:如何以及何时摆脱文件)。 和你一样,我的数据很复杂,嵌套的JSON对象很难放入表单中。

我所做的是创build两个服务器function。 第一个validation的数据。 如果出现错误,将返回。 如果这不是一个错误,我返回所有参数序列化/编码为base64string。 然后,在客户端,我有一个窗体只有一个隐藏的input,并张贴到第二个服务器function。 我将隐藏的input设置为base64string并提交格式。 第二个服务器function解码/反序列化参数并生成文件。 表单可以提交到一个新的窗口或页面上的iframe,文件将打开。

还有一点点的工作,也许还有一点点的处理,但总的来说,我觉得这个解决scheme更好。

代码在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); } 

在客户端

  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 } 

有一种更简单的方法,创build一个表单并发布,如果返回MIMEtypes是浏览器打开的话,就会有重置页面的风险,但对于csv而言,这是完美的

例子需要下划线和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(); 

对于像HTML,文本等这样的东西,确保MIMEtypes是应用程序/八位字节stream的东西

PHP代码

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

总之,没有简单的方法。 您需要提出另一个服务器请求来显示PDF文件。 虽然,有几个select,但他们不完美,不会在所有的浏览器上工作:

  1. 看看数据URIscheme 。 如果二进制数据很小,那么你也许可以使用JavaScript打开通过URI传递数据的窗口。
  2. 只有Windows / IE的解决scheme是使用.NET控件或FileSystemObject将数据保存在本地文件系统上,并从那里打开它。

自问这个问题已经有一段时间了,但我有同样的挑战,想分享我的解决scheme。 它使用来自其他答案的元素,但我无法完整地find它。 它不使用表单或iframe,但它确实需要一个post / get请求对。 它不是在请求之间保存文件,而是保存发布数据。 这似乎既简单又有效。

客户

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

服务器

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

我认为最好的方法是使用组合,你的第二种方法似乎是浏览器涉及的优雅解决scheme。

所以取决于如何打电话。 (无论是浏览器还是Web服务调用),您都可以使用这两者的组合,向浏览器发送URL并将原始数据发送到任何其他Web服务客户端。

并不完全是对原始文章的回答,而是一个将json对象发布到服务器并dynamic生成下载的快速而肮脏的解决scheme。

客户端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(); } 

..然后在服务器端解码json-string并设置头文件以供下载(PHP示例):

 $request = json_decode($_POST['jsonstring']), true); header('Content-Type: application/csv'); header('Content-Disposition: attachment; filename=export.csv'); header('Pragma: no-cache'); 

我已经清醒了两天,现在试图找出如何使用jQuery与ajax调用下载文件。 所有的支持,我不能帮助我的情况,直到我尝试这个。

客户端

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

另一种方法,而不是将文件保存在服务器上并检索它,是使用.NET 4.0 + ObjectCache与一个短的到期,直到第二个动作(在这个时候它可以被明确倾倒)。 我想使用JQuery Ajax进行调用的原因是它是asynchronous的。 构build我的dynamicPDF文件需要相当多的时间,并且在此期间显示一个忙碌的微调对话框(还可以完成其他工作)。 使用“成功:”中返回的数据来创buildBlob的方法无法可靠地工作。 这取决于PDF文件的内容。 如果它不是完整的文本,而是Ajax可以处理的,则很容易被响应中的数据破坏。