JavaScript/jQuery to download file via POST with JSON data
我有一个基于jquery的单页webapp。它通过Ajax调用与RESTful Web服务通信。
我正在尝试完成以下任务:
我现在有1&2工作,客户机jquery应用程序通过基于JSON数据创建DOM元素来在网页中显示返回的数据。我还有3从Web服务的角度工作,这意味着如果给定正确的JSON参数,它将创建并返回一个二进制文件。但我不确定在客户机JavaScript代码中处理3的最佳方法。
是否可以从这样的Ajax调用中获取可下载文件?如何让浏览器下载和保存文件?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | $.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; } }); |
服务器用以下头响应:
1 2 3 4 | Content-Disposition:attachment; filename=export-1282022272283.pdf Content-Length:5120 Content-Type:application/pdf Server:Jetty(6.1.11) |
另一个想法是生成PDF并将其存储在服务器上,然后返回包含文件URL的JSON。然后,在Ajax成功处理程序中发出另一个调用,执行如下操作:
1 2 3 | success: function(json,status) { window.location.href = json.url; } |
但是这样做意味着我需要对服务器进行不止一次的调用,我的服务器需要构建可下载的文件,将它们存储在某个地方,然后定期清理存储区域。
必须有一种更简单的方法来实现这一点。思想?
编辑:在查看了$.ajax的文档之后,我发现响应数据类型只能是
所以我想我的选择是:
不要使用Ajax,而是提交一个表单发布,并将我的JSON数据嵌入表单值中。可能需要处理隐藏的iframes等。
不要使用Ajax,而是将我的JSON数据转换成一个查询字符串来构建一个标准的GET请求,并将window.location.href设置为此URL。可能需要在我的单击处理程序中使用event.preventDefault(),以防止浏览器从应用程序URL更改。
使用我上面的其他想法,但通过@naikus答案的建议进行了增强。提交带有一些参数的Ajax请求,这些参数使Web服务知道这是通过Ajax调用调用的。如果从Ajax调用调用Web服务,只需返回带有生成资源URL的JSON。如果直接调用资源,则返回实际的二进制文件。
我想得越多,我就越喜欢最后的选择。通过这种方式,我可以获得有关请求的信息(生成时间、文件大小、错误消息等),并且可以在开始下载之前对这些信息进行操作。缺点是服务器上有额外的文件管理。
还有其他方法来完成这个吗?我应该知道这些方法的优缺点吗?
Letronje的解决方案只适用于非常简单的页面。
1 2 3 4 5 6 | $.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
1 2 3 | $.post('/create_binary_file.php', postData, function(retData) { $("body").append("<iframe src='" + retData.url+"' style='display: none;' ></iframe>"); }); |
实际操作:使用变量post data中的数据执行post-to/create_binary_file.php;如果成功完成了该post,请在页面正文中添加一个新的iframe。假设来自/create_binary_file.php的响应将包含一个值"url",该值是生成的pdf/xls/etc文件可以从中下载的url。向引用该URL的页面添加iframe将导致浏览器升级用户下载该文件,前提是Web服务器具有适当的mime类型配置。
我一直在玩另一个使用斑点的选项。我已经设法让它下载文本文档,并且我已经下载了PDF(不管它们是如何损坏的)。
使用BLOBAPI,您将能够执行以下操作:
1 2 3 4 5 6 7 8 9 | $.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中的文件。这将使用锚标记上的下载属性强制浏览器下载它。
我知道这有点老,但我想我已经想出了一个更优雅的解决方案。我也有同样的问题。我在建议的解决方案中遇到的问题是,它们都要求将文件保存在服务器上,但我不想将文件保存在服务器上,因为它引入了其他问题(安全性:文件随后可由未经身份验证的用户访问,清理:如何以及何时清除文件)。和您一样,我的数据也是复杂的、嵌套的JSON对象,很难放入表单中。
我所做的是创建两个服务器函数。第一个验证了数据。如果有错误,它将被返回。如果不是一个错误,我返回了所有序列化/编码为base64字符串的参数。然后,在客户机上,我有一个表单,它只有一个隐藏的输入,并发布到第二个服务器函数。我将隐藏输入设置为base64字符串并提交格式。第二个服务器函数解码/反序列化参数并生成文件。表单可以提交到新窗口或页面上的iframe,文件将打开。
有更多的工作要做,也许还有更多的处理,但总的来说,我对这个解决方案感觉好多了。
代码以c/mvc为单位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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); } |
在客户身上
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 | 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 } |
有一种更简单的方法,创建一个表单并发布它,如果返回的mime类型是浏览器会打开的类型,那么就有重置页面的风险,但是对于csv这样的类型,它是完美的。
示例需要下划线和jquery
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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、文本等内容,请确保mimetype类似于application/octet流。
PHP代码
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 | <?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文件。尽管如此,还是有一些选择,但它们并不完美,不能在所有浏览器上工作:
问这个问题已经有一段时间了,但我也有同样的挑战,我想分享我的解决方案。它使用了其他答案中的元素,但我找不到完整的答案。它不使用表单或iframe,但需要post/get请求对。它不在请求之间保存文件,而是保存日志数据。它看起来既简单又有效。
客户机1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; }); }); |
服务器
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 | [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"); } |
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 49 50 | $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 Unable to download CSV from POST " + 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(''); 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); }; |
不完全是对原始文章的回答,而是一个快速而肮脏的解决方案,用于将JSON对象发布到服务器并动态生成下载。
客户端jquery:
1 2 3 4 5 6 7 8 9 | var download = function(resource, payload) { $("#downloadFormPoster").remove(); $("<iframe name='downloadFormPosterIframe'></iframe>").appendTo('body'); $("<form action='" + resource +"' target='downloadFormPosterIframe' method='post'>" + "<input type='hidden' name='jsonstring' value='" + JSON.stringify(payload) +"'/>" + "</form>") .appendTo("#downloadFormPoster") .submit(); } |
…然后在服务器端解码JSON字符串并设置下载头(PHP示例):
1 2 3 4 | $request = json_decode($_POST['jsonstring']), true); header('Content-Type: application/csv'); header('Content-Disposition: attachment; filename=export.csv'); header('Pragma: no-cache'); |
我认为最好的方法是使用组合,第二种方法似乎是一种优雅的解决方案,其中涉及到浏览器。
所以这取决于通话的方式。(无论是浏览器还是Web服务调用)您可以使用这两者的组合,将URL发送到浏览器,并将原始数据发送到任何其他Web服务客户端。
使用HTML5,您只需创建一个锚并点击它。不需要将其作为子级添加到文档中。
1 2 3 4 | const a = document.createElement('a'); a.download = ''; a.href = urlForPdfFile; a.click(); |
都做完了。
如果您想为下载提供一个特殊的名称,只需在
1 2 3 4 | const a = document.createElement('a'); a.download = 'my-special-name.pdf'; a.href = urlForPdfFile; a.click(); |
我已经醒了两天了,现在我想知道如何使用jquery with ajax调用下载一个文件。我得到的所有支持对我的处境都无济于事,直到我尝试了这个。
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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); } }); } |
服务器端
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 | [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. } |
祝你好运。
另一种方法不是将文件保存在服务器上并检索它,而是使用.NET 4.0+ObjectCache,其有效期很短,直到执行第二个操作为止(此时可以确定地将其转储)。我想使用jqueryajax来进行调用的原因是它是异步的。构建动态PDF文件需要相当长的时间,在此期间我会显示一个繁忙的微调器对话框(它还允许完成其他工作)。使用"success:"中返回的数据创建blob的方法不可靠。这取决于PDF文件的内容。如果响应中的数据不完全是文本的,那么它很容易被破坏,而这正是Ajax所能处理的。