JavaScript / jQuery para baixar o arquivo via POST com dados JSON

Eu tenho um webapp de página única baseado em jquery. Comunica-se com um serviço web RESTful por meio de chamadas AJAX.

Estou tentando realizar o seguinte:

  1. Envie um POST que contenha dados JSON para um URL REST.
  2. Se a solicitação especificar uma resposta JSON, o JSON será retornado.
  3. Se a solicitação especificar uma resposta PDF / XLS / etc, um binário baixável será retornado.

Eu tenho 1 e 2 trabalhando agora, e o aplicativo cliente jquery exibe os dados retornados na página da web criando elementos DOM com base nos dados JSON. Eu também tenho o # 3 funcionando do ponto de vista do serviço da web, o que significa que ele criará e retornará um arquivo binário se receber os parâmetros JSON corretos. Mas não tenho certeza da melhor maneira de lidar com o # 3 no código javascript do cliente.

É possível recuperar um arquivo para download de uma chamada ajax como essa? Como obtenho o navegador para baixar e salvar o arquivo?

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

O servidor responde com os seguintes headers:

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

Outra ideia é gerar o PDF e armazená-lo no servidor e retornar o JSON que inclui uma URL para o arquivo. Em seguida, emita outra chamada no manipulador de sucesso do ajax para fazer algo como o seguinte:

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

Mas fazer isso significa que eu precisaria fazer mais de uma chamada para o servidor, e meu servidor precisaria criar arquivos para download, armazená-los em algum lugar e depois limpar periodicamente a área de armazenamento.

Deve haver uma maneira mais simples de conseguir isso. Idéias?


EDIT: Depois de analisar os documentos para $ .ajax, vejo que a resposta dataType só pode ser um dos xml, html, script, json, jsonp, text , então eu estou supondo que não há nenhuma maneira de baixar diretamente um arquivo usando um ajax request, a menos que eu inclua o arquivo binário usando o esquema URI de dados, como sugerido na resposta @VinayC (que não é algo que eu queira fazer).

Então eu acho que minhas opções são:

  1. Não use ajax e, em vez disso, envie uma postagem de formulário e incorpore meus dados JSON aos valores do formulário. Provavelmente precisaria mexer com iframes escondidos e tal.

  2. Não use ajax e converta meus dados JSON em uma string de consulta para criar uma solicitação GET padrão e defina window.location.href para essa URL. Pode ser necessário usar o event.preventDefault () no meu manipulador de cliques para impedir que o navegador mude do URL do aplicativo.

  3. Use minha outra ideia acima, mas aprimore com sugestões da resposta do @naikus. Envie a solicitação AJAX com algum parâmetro que permita que o serviço da web saiba que isso está sendo chamado por meio de uma chamada ajax. Se o serviço da web for chamado a partir de uma chamada ajax, simplesmente retorne JSON com uma URL para o recurso gerado. Se o recurso for chamado diretamente, retorne o arquivo binário real.

Quanto mais penso nisso, mais gosto da última opção. Dessa forma, posso obter informações sobre a solicitação (tempo para gerar, tamanho do arquivo, mensagens de erro, etc.) e posso agir com base nessas informações antes de iniciar o download. A desvantagem é o gerenciamento extra de arquivos no servidor.

Alguma outra maneira de conseguir isso? Quaisquer prós / contras para esses methods que eu deveria estar ciente?

A solução da letronje só funciona para páginas muito simples. document.body.innerHTML += pega o texto HTML do corpo, acrescenta o HTML do iframe e define o innerHTML da página para essa string. Isso eliminará todas as ligações de events da sua página, entre outras coisas. Crie um elemento e use 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); }); 

Ou usando jQuery

 $.post('/create_binary_file.php', postData, function(retData) { $("body").append(""); }); 

O que isso realmente faz: executar um post para /create_binary_file.php com os dados na variável postData; Se essa postagem for concluída com êxito, adicione um novo iframe ao corpo da página. A suposição é que a resposta de /create_binary_file.php includeá um valor ‘url’, que é a URL da qual o arquivo PDF / XLS / etc gerado pode ser baixado. Adicionar um iframe à página que faz referência a essa URL fará com que o navegador promova o usuário a fazer o download do arquivo, supondo que o servidor da Web tenha a configuração de tipo mime apropriada.

Eu tenho brincado com outra opção que usa blobs. Consegui fazer o download de documentos de texto e baixei o PDF (no entanto, eles estão corrompidos).

Usando a API de blob, você poderá fazer o seguinte:

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

Este é o IE 10+, o Chrome 8+, o FF 4+. Veja https://developer.mozilla.org/pt-BR/docs/Web/API/URL.createObjectURL

Ele só baixará o arquivo no Chrome, Firefox e Opera. Isso usa um atributo de download na tag de âncora para forçar o navegador a baixá-lo.

Eu conheço esse tipo de coisa antiga, mas acho que encontrei uma solução mais elegante. Eu tive o mesmo problema. O problema que eu estava tendo com as soluções sugeridas era que todas exigiam que o arquivo fosse salvo no servidor, mas eu não queria salvar os arquivos no servidor, porque ele introduziu outros problemas (segurança: o arquivo poderia ser acessado por usuários não autenticados, limpeza: como e quando você se livra dos arquivos). E, como você, meus dados eram objects JSON complexos e nesteds que seriam difíceis de colocar em um formulário.

O que eu fiz foi criar duas funções de servidor. O primeiro validou os dados. Se houve um erro, ele será retornado. Se não foi um erro, retornei todos os parâmetros serializados / codificados como uma string base64. Então, no cliente, tenho um formulário que tem apenas uma input oculta e posta em uma segunda function de servidor. Eu defino a input oculta para a string base64 e submeto o formato. A segunda function do servidor decodifica / desserializa os parâmetros e gera o arquivo. O formulário pode ser enviado para uma nova janela ou um iframe na página e o arquivo será aberto.

Há um pouco mais de trabalho envolvido e, talvez, um pouco mais de processamento, mas, no geral, me senti muito melhor com essa solução.

Código está em 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().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().ToEntity(data); // TODO: Generate file return File(bytes, mimeType); } 

no cliente

  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 } 

Existe uma maneira mais simples, criar um formulário e publicá-lo, isso corre o risco de redefinir a página se o tipo de retorno do mime é algo que um navegador abriria, mas para csv e tal é perfeito

Exemplo requer sublinhado e jquery

 var postData = { filename:filename, filecontent:filecontent }; var fakeFormHtmlFragment = "
"; _.each(postData, function(postValue, postKey){ var escapedKey = postKey.replace("\\", "\\\\").replace("'", "\'"); var escapedValue = postValue.replace("\\", "\\\\").replace("'", "\'"); fakeFormHtmlFragment += ""; }); fakeFormHtmlFragment += "
"; $fakeFormDom = $(fakeFormHtmlFragment); $("body").append($fakeFormDom); $fakeFormDom.submit();

Para coisas como html, text e tal, certifique-se de que o tipo MIME seja algo como application / octet-stream

código 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; 

Em suma, não há maneira mais simples. Você precisa fazer outra solicitação do servidor para mostrar o arquivo PDF. No entanto, existem poucas alternativas, mas elas não são perfeitas e não funcionam em todos os navegadores:

  1. Veja o esquema de URI de dados . Se os dados binários forem pequenos, talvez você possa usar o javascript para abrir os dados de passagem da janela no URI.
  2. Windows / IE apenas solução seria ter o controle .net ou FileSystemObject para salvar os dados no sistema de arquivos local e abri-lo de lá.

Já faz um tempo que essa pergunta foi feita, mas eu tive o mesmo desafio e quero compartilhar minha solução. Ele usa elementos das outras respostas, mas não consegui encontrá-lo em sua totalidade. Ele não usa um formulário ou um iframe, mas exige um par de solicitações post / get. Em vez de salvar o arquivo entre as solicitações, ele salva os dados da postagem. Parece ser simples e eficaz.

cliente

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

servidor

 [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(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('
'); 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(''); //csv_window.document.write(' '); //csv_window.document.write(response.data); };

Eu acho que a melhor abordagem é usar uma combinação, sua segunda abordagem parece ser uma solução elegante, onde os navegadores estão envolvidos.

Então, dependendo de como a chamada é feita. (seja um navegador ou uma chamada de serviço da web) você pode usar uma combinação dos dois, enviar uma URL para o navegador e enviar dados brutos para qualquer outro cliente de serviço da web.

Não totalmente uma resposta para o post original, mas uma solução rápida e suja para postar um object json no servidor e gerar dinamicamente um download.

JQuery do lado do cliente:

 var download = function(resource, payload) { $("#downloadFormPoster").remove(); $("").appendTo('body'); $("
" + "" + "
") .appendTo("#downloadFormPoster") .submit(); }

..e então decodificando a cadeia json no serveride e configurando os headers para download (exemplo do PHP):

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

Eu estou acordado há dois dias tentando descobrir como baixar um arquivo usando o jquery com ajax call. Todo o apoio que recebi não poderia ajudar a minha situação até que eu tente isso.

Lado do Cliente

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

Outra abordagem, em vez de salvar o arquivo no servidor e recuperá-lo, é usar o .NET 4.0+ ObjectCache com uma expiração curta até a segunda Ação (momento em que pode ser definitivamente descartado). O motivo pelo qual desejo usar o JQuery Ajax para fazer a chamada é que ele é asynchronous. Construindo meu arquivo PDF dynamic leva um pouco de tempo, e eu exibo uma checkbox de diálogo girando ocupado durante esse tempo (também permite que outro trabalho seja feito). A abordagem de usar os dados retornados no “sucesso:” para criar um Blob não funciona de maneira confiável. Depende do conteúdo do arquivo PDF. É facilmente corrompido pelos dados na resposta, se não for completamente textual, o que é tudo o que o Ajax pode manipular.

Com o HTML5, você pode simplesmente criar uma âncora e clicar nela. Não há necessidade de adicioná-lo ao documento como filho.

 const a = document.createElement('a'); a.download = ''; a.href = urlForPdfFile; a.click(); 

Tudo feito.

Se você quiser ter um nome especial para o download, basta passá-lo no atributo de download :

 const a = document.createElement('a'); a.download = 'my-special-name.pdf'; a.href = urlForPdfFile; a.click();