JavaScript / jQuery per scaricare il file tramite POST con dati JSON


250

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 che contenga 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 client jquery 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 dati i parametri JSON corretti. Ma non sono sicuro del modo migliore per gestire il n. 3 nel codice javascript del client.

È possibile recuperare un file scaricabile da una chiamata Ajax come questa? Come posso 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 è quella di generare il PDF e archiviarlo sul server e restituire JSON che include un URL al file. Quindi, invia un'altra chiamata nel gestore del 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 effettuare più di una chiamata al server e il mio server avrebbe bisogno di creare file scaricabili, archiviarli da qualche parte, quindi ripulire periodicamente quell'area di archiviazione.

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


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

Quindi immagino che le mie opzioni siano:

  1. Non usare Ajax e invece inviare un post di modulo e incorporare i miei dati JSON nei valori del modulo. Probabilmente avrebbe bisogno di pasticciare con iframe nascosti e simili.

  2. Non usare Ajax e invece 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 che il browser cambi dall'URL dell'applicazione.

  3. Usa la mia altra idea sopra, ma migliorata con i suggerimenti della risposta @naikus. Invia la richiesta AJAX con alcuni parametri che consentono al servizio web di sapere che 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, restituisce il file binario effettivo.

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

Altri modi per raggiungere questo obiettivo? Qualche pro / contro di questi metodi di cui dovrei essere a conoscenza?



6
questo era "un po '" prima, quindi come è un duplicato
GiM

1
url = 'http://localhost/file.php?file='+ $('input').val(); window.open(url); Il modo semplice per ottenere gli stessi risultati. Inserisci l'intestazione nel file php. Non è necessario inviare ajax o ricevere richieste
abhishek bagul,

Risposte:


171

La soluzione di letronje funziona solo con pagine molto semplici. document.body.innerHTML +=prende il testo HTML del corpo, aggiunge l'HTML iframe e imposta l'HTML interno della pagina su quella stringa. Ciò eliminerà, tra le altre cose, eventuali vincoli di eventi della tua pagina. Crea un elemento e usa appendChildinvece.

$.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("<iframe src='" + retData.url+ "' style='display: none;' ></iframe>");
}); 

Cosa fa realmente: esegui un post in /create_binary_file.php con i dati nella variabile postData; se quel post viene completato correttamente, aggiungi un nuovo iframe al corpo della pagina. Il presupposto è che la risposta da /create_binary_file.php includerà un valore 'url', che è l'URL da cui è possibile scaricare il file PDF / XLS / etc generato. Aggiungendo un iframe alla pagina che fa riferimento a tale URL, il browser promuoverà l'utente a scaricare il file, supponendo che il server Web abbia la configurazione del tipo mime appropriata.


1
Sono d'accordo, è meglio che usare +=e aggiornerò la mia domanda di conseguenza. Grazie!
Tauren,

3
Mi piace questo concetto, tuttavia Chrome ha questo messaggio nella console: Resource interpreted as Document but transferred with MIME type application/pdf. Quindi mi avverte anche che il file potrebbe essere pericoloso. Se visito direttamente retData.url, nessun avviso o problema.
Michael Irey,

Guardando in giro per Internet, sembra che potresti avere problemi con i PDF in iFrames se il tuo server non sta impostando correttamente Content-Type e Content-Disposition. Non ho usato questa soluzione da solo, ho appena chiarito una soluzione precedente, quindi non ho altri consigli, temo.
SamStephens,

1
@Tauren: se sei d'accordo che questa è una risposta migliore, tieni presente che puoi cambiare la risposta accettata in qualsiasi momento - o persino rimuovere del tutto il segno di accettazione. Basta spuntare la nuova risposta e il segno di accettazione verrà trasferito. (Vecchia domanda, ma è stata recentemente
evidenziata

5
che cosa dovrebbe essere retData.url?
Mark Thien

48

Ho giocato con un'altra opzione che utilizza i BLOB. Sono riuscito a scaricarlo per scaricare documenti di testo e ho scaricato PDF (tuttavia sono danneggiati).

Usando 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à il file solo su Chrome, Firefox e Opera. Questo utilizza un attributo di download sul tag anchor per forzare il download del browser.


1
Nessun supporto per l'attributo di download su IE e Safari :( ( caniuse.com/#feat=download ). Puoi aprire una nuova finestra e inserire contenuti lì, ma non sei sicuro di un PDF o Excel ...
Marçal Juan

1
Dovresti liberare memoria del browser dopo aver chiamato click:window.URL.revokeObjectURL(link.href);
Edward Brey,

@JoshBerke Questo è vecchio ma funziona da parte mia. Come posso farlo per aprire immediatamente la mia directory per salvare piuttosto che salvare sul mio browser? C'è anche un modo per determinare il nome del file in arrivo?
Jo Ko,

2
Danneggerà i file binari, perché vengono convertiti in stringhe utilizzando la codifica e il risultato non è molto vicino al binario originale ...
Gobol,

2
Può utilizzare i mimetipi per non corrompere le informazioni utilizzate qui con i PDF: alexhadik.com/blog/2016/7/7/l8ztp8kr5lbctf5qns4l8t3646npqh
Mateo Tibaquira,

18

Conosco questo tipo di vecchio, 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 tutti richiedevano il salvataggio del file sul server, ma non volevo salvare i file sul server, perché introduceva altri problemi (sicurezza: il file poteva essere accessibile da utenti non autenticati, pulizia: come e quando sbarazzarsi dei 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 si fosse verificato un errore, verrà restituito. Se non è stato un errore, ho restituito tutti i parametri serializzati / codificati come stringa base64. Quindi, sul client, ho un modulo che ha solo un input nascosto e inserisce una seconda funzione del server. Ho impostato l'input nascosto sulla stringa base64 e ho inviato il formato. La seconda funzione del server decodifica / deserializza i parametri e genera il file. Il modulo potrebbe essere inviato a una nuova finestra o a un iframe nella pagina e il file si aprirà.

C'è bisogno di un po 'più di lavoro, 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<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);
    }

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
    }

1
Non ho dato una buona occhiata a questa soluzione, ma vale la pena notare che nulla della soluzione che utilizza create_binary_file.php richiede che i file vengano salvati su disco. Sarebbe del tutto possibile che create_binary_file.php generi file binari in memoria.
SamStephens,

11

C'è un modo più semplice, crea un modulo e pubblicalo, questo corre il rischio di reimpostare la pagina se il tipo mime di ritorno è qualcosa che un browser potrebbe aprire, ma per CSV e tale è perfetto

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

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

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

8

In breve, non esiste un modo più semplice. È necessario effettuare un'altra richiesta del server per mostrare il file PDF. Tuttavia, ci sono poche alternative ma non sono perfette e non funzioneranno su tutti i browser:

  1. Guarda lo schema URI dei dati . Se i dati binari sono piccoli, puoi forse usare JavaScript per aprire la finestra che passa i dati nell'URI.
  2. L'unica soluzione per Windows / IE sarebbe quella di avere il controllo .NET o FileSystemObject per salvare i dati sul file system locale e aprirli da lì.

Grazie, non ero a conoscenza dello schema URI dei dati. Sembra che potrebbe essere l'unico modo per farlo in una singola richiesta. Ma non è proprio la direzione che voglio andare.
Tauren,

A causa delle esigenze del cliente ho finito per risolvere questo problema usando le intestazioni del server in response ( Content-Disposition: attachment;filename="file.txt"), ma voglio davvero provare questo approccio la prossima volta. URIPrima avevo usato i dati per le anteprime, ma non mi è mai venuto in mente di usarli per i download, grazie per aver condiviso questo nugget.
brichins,

8

È passato un po 'di tempo da quando questa domanda è stata posta, ma ho avuto la stessa sfida e voglio condividere la mia soluzione. Utilizza elementi delle altre risposte ma non sono riuscito a trovarlo nella sua interezza. Non utilizza un modulo o un iframe ma richiede una coppia richiesta post / get. Invece di salvare il file tra le richieste, salva i dati di 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<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
Penso che questa soluzione mi piaccia di più. Nel mio caso, tuttavia, sto generando il file, quindi penso che proverò a mantenere il file in memoria fino a quando non verrà effettuato l'accesso, quindi a liberare la memoria dopo il download, quindi non devo mai scrivere sul disco.
BVernon,

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

3

Non del tutto una risposta al post originale, ma una soluzione rapida e sporca per pubblicare un oggetto json sul server e generare dinamicamente un download.

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

..e quindi decodifica la stringa json sul lato server e impostazione delle 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');

Mi piace questa soluzione. Nella domanda dell'OP sembra che il richiedente sappia se aspettarsi un download di file o dati JSON, quindi potrebbe decidere sul lato client e pubblicare su un URL diverso, piuttosto che decidere sul lato server.
Sam,

2

Penso che l'approccio migliore sia usare una combinazione, il tuo secondo approccio sembra essere una soluzione elegante in cui sono coinvolti i browser.

Quindi, a seconda di come viene effettuata la chiamata. (che si tratti di un browser o di una chiamata al servizio Web) è possibile utilizzare una combinazione dei due, con l'invio di un URL al browser e l'invio di dati non elaborati a qualsiasi altro client del servizio Web.


Anche il mio secondo approccio sembra più attraente anche per me. Grazie per aver confermato che è una soluzione utile. Stai suggerendo di passare un valore aggiuntivo nell'oggetto json che indica che questa richiesta viene effettuata da un browser come una chiamata Ajax anziché una chiamata di servizio Web? Ci sono diversi modi per realizzare ciò che mi viene in mente, ma quale tecnica useresti?
Tauren,

non può essere determinato dall'intestazione http User-Agent? O qualsiasi altra intestazione http?
naikus,

1

Sono stato sveglio per due giorni ora cercando 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);
        }
    });

}

Lato server

 [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.
    }

In bocca al lupo.


2
Funzionerà, per file di piccole dimensioni. Ma se hai un file di grandi dimensioni ti imbatterai nel problema che l'URL è troppo lungo. Provalo con 1.000 record, si romperà ed esporterà solo parte dei dati.
Michael Merrell,

1

L'ho trovato da qualche parte molto tempo fa e funziona perfettamente!

let payload = {
  key: "val",
  key2: "val2"
};

let url = "path/to/api.php";
let form = $('<form>', {'method': 'POST', 'action': url}).hide();
$.each(payload, (k, v) => form.append($('<input>', {'type': 'hidden', 'name': k, 'value': v})) );
$('body').append(form);
form.submit();
form.remove();

0

Un altro approccio invece di salvare il file sul server e recuperarlo, è usare .NET 4.0+ ObjectCache con una breve scadenza fino alla seconda azione (a quel punto può essere definitivamente scaricato). Il motivo per cui voglio usare JQuery Ajax per fare la chiamata è che è asincrono. La creazione del mio file PDF dinamico richiede un bel po 'di tempo e durante quel periodo visualizzo una finestra di dialogo con lo spinner occupato (che consente anche di svolgere altri lavori). L'approccio dell'utilizzo 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.


0

Soluzione

L'allegato sulla disposizione dei contenuti sembra funzionare per me:

self.set_header("Content-Type", "application/json")
self.set_header("Content-Disposition", 'attachment; filename=learned_data.json')

Soluzione

application / octet-stream

Mi è successo qualcosa di simile a me con un JSON, per me sul lato server stavo impostando l'intestazione su self.set_header ("Content-Type", " application / json ") tuttavia quando l'ho cambiato in:

self.set_header("Content-Type", "application/octet-stream")

Lo ha scaricato automaticamente.

Inoltre, affinché il file mantenga ancora il suffisso .json, sarà necessario nell'intestazione del nome file:

self.set_header("Content-Disposition", 'filename=learned_data.json')

-1

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

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 downloadnell'attributo:

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

4
la domanda riguarda il metodo POST
dovid
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.