JavaScript / jQuery per scaricare file tramite POST con dati JSON

Ho una webapp a pagina singola basata su jQuery. Comunica con un servizio web RESTful tramite chiamate AJAX.

Sto cercando di realizzare quanto segue:

  1. Invia un POST contenente dati JSON a un URL REST.
  2. Se la richiesta specifica una risposta JSON, viene restituito JSON.
  3. Se la richiesta specifica una risposta PDF / XLS / etc, viene restituito un file binario scaricabile.

Ora lavoro 1 e 2 e l’app jquery client visualizza i dati restituiti nella pagina Web creando elementi DOM basati sui dati JSON. Ho anche # 3 funzionante dal punto di vista del servizio web, il che significa che creerà e restituirà un file binario se fornito dei parametri JSON corretti. Ma non sono sicuro del modo migliore per gestire il # 3 nel codice javascript del client.

È ansible recuperare un file scaricabile da una chiamata ajax come questa? Come faccio a scaricare e salvare il file dal browser?

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

Il server risponde con le seguenti intestazioni:

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

Un’altra idea è generare il PDF e memorizzarlo sul server e restituire JSON che include un URL al file. Quindi, invia un’altra chiamata al gestore di successo di ajax per fare qualcosa di simile al seguente:

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

Ma ciò significa che avrei bisogno di fare più di una chiamata al server, e il mio server avrebbe bisogno di creare file scaricabili, archiviarli da qualche parte, quindi ripulire periodicamente l’area di archiviazione.

Ci deve essere un modo più semplice per realizzare questo. Idee?


EDIT: dopo aver esaminato i documenti per $ .ajax, vedo che la risposta dataType può essere solo uno di xml, html, script, json, jsonp, text , quindi xml, html, script, json, jsonp, text che non ci sia modo di scaricare direttamente un file usando un richiesta ajax, a meno che non incorpori il file binario nell’uso dello schema URI dati come suggerito nella risposta @VinayC (che non è qualcosa che voglio fare).

Quindi immagino che le mie opzioni siano:

  1. Non utilizzare ajax e inviare invece un post del modulo e incorporare i miei dati JSON nei valori del modulo. Probabilmente avrebbe bisogno di fare casino con iframe nascosti e così via.

  2. Non utilizzare ajax e convertire i miei dati JSON in una stringa di query per creare una richiesta GET standard e impostare window.location.href su questo URL. Potrebbe essere necessario utilizzare event.preventDefault () nel mio gestore di clic per impedire al browser di cambiare dall’URL dell’applicazione.

  3. Usa la mia altra idea sopra, ma arricchita con i suggerimenti della risposta @naikus. Invia una richiesta AJAX con alcuni parametri che consentono al servizio web di sapere che questo viene chiamato tramite una chiamata Ajax. Se il servizio web viene chiamato da una chiamata Ajax, è sufficiente restituire JSON con un URL alla risorsa generata. Se la risorsa viene chiamata direttamente, restituire il file binario effettivo.

Più ci penso, più mi piace l’ultima opzione. In questo modo posso ottenere informazioni sulla richiesta (tempo di generazione, dimensioni del file, messaggi di errore, ecc.) E posso agire su tali informazioni prima di iniziare il download. Lo svantaggio è la gestione extra dei file sul server.

Qualche altro modo per realizzare questo? Dovrei essere a conoscenza di eventuali pro / contro su questi metodi?

La soluzione di letronje funziona solo per pagine molto semplici. document.body.innerHTML += prende il testo HTML del corpo, aggiunge l’iframe HTML e imposta l’innerHTML della pagina su quella stringa. Questo cancellerà qualsiasi legame agli eventi che la tua pagina ha, tra le altre cose. Crea un elemento e usa invece 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); }); 

O usando jQuery

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

Cosa fa questo: esegui un post su /create_binary_file.php con i dati nella variabile postData; se il post viene completato correttamente, aggiungi un nuovo iframe al corpo della pagina. L’ipotesi è che la risposta da /create_binary_file.php includa un valore ‘url’, che è l’URL dal quale è ansible scaricare il file PDF / XLS / etc generato. Aggiungendo un iframe alla pagina che fa riferimento a tale URL, il browser promuoverà all’utente di scaricare il file, assumendo che il server Web disponga della configurazione del tipo mime appropriata.

Ho giocato con un’altra opzione che usa BLOB. Sono riuscito a scaricarlo per scaricare documenti di testo e ho scaricato i PDF (comunque sono corrotti).

Utilizzando l’API blob sarai in grado di fare quanto segue:

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

Questo è IE 10+, Chrome 8+, FF 4+. Vedi https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL

Scaricherà solo il file in Chrome, Firefox e Opera. Questo utilizza un attributo download sul tag anchor per forzare il browser a scaricarlo.

Conosco questo tipo di età, ma penso di aver trovato una soluzione più elegante. Ho avuto lo stesso identico problema. Il problema che stavo riscontrando con le soluzioni suggerite era che richiedevano tutti il ​​salvataggio del file sul server, ma non volevo salvare i file sul server, perché presentavano altri problemi (sicurezza: il file poteva quindi essere accessibile utenti non autenticati, pulizia: come e quando si eliminano i file). E come te, i miei dati erano oggetti JSON complessi e nidificati che sarebbero stati difficili da inserire in un modulo.

Quello che ho fatto è stato creare due funzioni del server. Il primo ha convalidato i dati. Se ci fosse stato un errore, sarebbe stato restituito. Se non si trattava di un errore, ho restituito tutti i parametri serializzati / codificati come una stringa base64. Quindi, sul client, ho un modulo con un solo input e post nascosti su una seconda funzione del server. Ho impostato l’input nascosto sulla stringa base64 e ho inviato il formato. La seconda funzione server decodifica / deserializza i parametri e genera il file. Il modulo potrebbe inviare una nuova finestra o un iframe nella pagina e il file si aprirà.

C’è un po ‘di lavoro in più, e forse un po’ più di elaborazione, ma nel complesso, mi sono sentito molto meglio con questa soluzione.

Il codice è in 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); } 

sul 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 } 

C’è un modo più semplice, creare un modulo e postarlo, questo comporta il rischio di reimpostare la pagina se il tipo mime di ritorno è qualcosa che un browser potrebbe aprire, ma per csv e così è perfetto

L’esempio richiede underscore 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();

Per cose come html, text e simili, assicurati che il mimetype sia qualcosa come application / octet-stream

codice php

  

In breve, non esiste un modo più semplice. Devi fare un’altra richiesta server per mostrare il file PDF. Però, ci sono poche alternative ma non sono perfette e non funzioneranno su tutti i browser:

  1. Guarda lo schema URI di dati . Se i dati binari sono piccoli, puoi forse usare javascript per aprire i dati di passaggio della finestra nell’URI.
  2. L’unica soluzione per Windows / IE sarebbe avere il controllo .NET o FileSystemObject per salvare i dati sul file system locale e aprirlo da lì.

È passato un po ‘di tempo da quando è stata posta questa domanda, ma ho avuto la stessa sfida e voglio condividere la mia soluzione. Usa elementi dalle altre risposte ma non sono riuscito a trovarlo nella sua interezza. Non usa un modulo o un iframe ma richiede una coppia di richieste post / get. Invece di salvare il file tra le richieste, salva i dati del post. Sembra essere sia semplice che efficace.

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

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

Penso che l’approccio migliore sia utilizzare una combinazione. Il tuo secondo approccio sembra essere una soluzione elegante in cui i browser sono coinvolti.

Quindi dipende dal modo in cui viene effettuata la chiamata. (sia esso un browser o una chiamata al servizio web) è ansible utilizzare una combinazione dei due, con l’invio di un URL al browser e l’invio di dati grezzi a qualsiasi altro client di servizi Web.

Non è interamente una risposta al post originale, ma una soluzione rapida e sporca per la pubblicazione di un object json sul server e la generazione dynamic di un download.

JQuery lato client:

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

..e quindi decodificare la stringa json sul server e impostare le intestazioni per il download (esempio PHP):

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

Sono stato sveglio per due giorni cercando ora di capire come scaricare un file usando jquery con una chiamata ajax. Tutto il supporto che ho ricevuto non ha potuto aiutare la mia situazione fino a quando non ho provato questo.

Dalla parte del 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); } }); } 

Un altro approccio, invece di salvare il file sul server e recuperarlo, è utilizzare .NET 4.0+ ObjectCache con una breve scadenza fino alla seconda azione (in cui può essere definitivamente scaricata). Il motivo per cui desidero utilizzare JQuery Ajax per effettuare la chiamata è che è asincrono. Costruire il mio file PDF dinamico richiede un po ‘di tempo, e durante questo periodo visualizzerò una finestra di dialogo molto triggers (consente anche di eseguire altri lavori). L’approccio all’uso dei dati restituiti nel “successo:” per creare un blob non funziona in modo affidabile. Dipende dal contenuto del file PDF. È facilmente corrotto dai dati nella risposta, se non è completamente testuale, che è tutto ciò che Ajax può gestire.

Con HTML5, puoi semplicemente creare un ancoraggio e fare clic su di esso. Non è necessario aggiungerlo al documento da bambino.

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

Tutto fatto.

Se vuoi avere un nome speciale per il download, basta passarlo nell’attributo download :

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