web-dev-qa-db-fra.com

JavaScript / jQuery pour télécharger le fichier via POST avec des données JSON

J'ai une application Web à page unique basée sur JQuery. Il communique avec un service Web RESTful via AJAX appels.

J'essaie d'accomplir ce qui suit:

  1. Soumettez un POST contenant des données JSON à une URL REST.
  2. Si la demande spécifie une réponse JSON, JSON est renvoyé.
  3. Si la demande spécifie une réponse PDF/XLS/etc, un fichier binaire téléchargeable est renvoyé.

J'ai 1 & 2 travaille maintenant, et l'application cliente jquery affiche les données renvoyées dans la page Web en créant des éléments DOM basés sur les données JSON. J'ai également la 3ème solution du point de vue des services Web, ce qui signifie qu’elle créera et renverra un fichier binaire si les paramètres JSON appropriés sont fournis. Mais je ne suis pas sûr de la meilleure façon de traiter le n ° 3 dans le code javascript du client.

Est-il possible de récupérer un fichier téléchargeable à partir d'un appel ajax comme celui-ci? Comment faire en sorte que le navigateur télécharge et enregistre le fichier?

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

Le serveur répond avec les en-têtes suivants:

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

Une autre idée consiste à générer le PDF, à le stocker sur le serveur et à renvoyer un fichier JSON contenant une URL vers le fichier. Ensuite, lancez un autre appel dans le gestionnaire de succès ajax pour effectuer une opération semblable à celle-ci:

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

Mais cela signifie que je devrais faire plus d’un appel au serveur et que mon serveur aurait besoin de créer des fichiers téléchargeables, de les stocker quelque part, puis de nettoyer périodiquement cette zone de stockage.

Il doit y avoir un moyen plus simple d'y parvenir. Des idées?


EDIT: Après avoir examiné la documentation pour $ .ajax, je constate que la réponse dataType ne peut être que l’un des xml, html, script, json, jsonp, text, donc je suppose qu’il n’ya aucun moyen de télécharger directement un fichier à l’aide d’une requête ajax, à moins d’y intégrer le fichier binaire en utilisant le schéma Data URI comme suggéré dans la réponse @VinayC (ce que je ne veux pas faire).

Donc, je suppose que mes options sont:

  1. N'utilisez pas ajax et envoyez plutôt un message de formulaire et intégrez mes données JSON aux valeurs du formulaire. Aurait probablement besoin de jouer avec des iframes cachés et autres.

  2. N'utilisez pas ajax et convertissez plutôt mes données JSON en chaîne de requête pour créer une demande GET standard et définir window.location.href sur cette URL. Il peut être nécessaire d’utiliser event.preventDefault () dans mon gestionnaire de clics pour empêcher le navigateur de changer d’URL de l’application.

  3. Utilisez mon autre idée ci-dessus, mais enrichie des suggestions de la réponse @naikus. Soumettez la requête AJAX avec un paramètre permettant au service Web de savoir qu'il est appelé via un appel ajax. Si le service Web est appelé à partir d'un appel ajax, renvoyez simplement JSON avec une URL vers la ressource générée. Si la ressource est appelée directement, renvoyez le fichier binaire actuel.

Plus j'y pense, plus j'aime la dernière option. De cette façon, je peux obtenir des informations sur la demande (temps de génération, taille du fichier, messages d'erreur, etc.) et pouvoir utiliser ces informations avant de commencer le téléchargement. L'inconvénient est la gestion de fichiers supplémentaire sur le serveur.

Y a-t-il d'autres moyens d'accomplir cela? Quels avantages/inconvénients de ces méthodes devrais-je connaître?

235
Tauren

La solution de letronje ne fonctionne que pour des pages très simples. document.body.innerHTML += prend le texte HTML du corps, ajoute l'iframe HTML et définit le innerHTML de la page sur cette chaîne. Cela effacera toutes les reliures d’événements de votre page, entre autres. Créez un élément et utilisez appendChild à la place.

$.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 en utilisant jQuery

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

En réalité, effectuez une publication dans /create_binary_file.php avec les données de la variable postData; si cette publication se termine correctement, ajoutez une nouvelle iframe au corps de la page. L'hypothèse est que la réponse de /create_binary_file.php inclura une valeur 'url', qui correspond à l'URL à partir de laquelle le fichier PDF/XLS/etc généré peut être téléchargé. Si vous ajoutez un iframe à la page qui fait référence à cette URL, le navigateur incitera l'utilisateur à télécharger le fichier, en supposant que le serveur Web dispose de la configuration de type mime appropriée.

167
SamStephens

J'ai joué avec une autre option qui utilise des blobs. J'ai réussi à l'obtenir pour télécharger des documents texte, et j'ai téléchargé des PDF (ils sont toutefois corrompus).

En utilisant l’API blob, vous pourrez effectuer les opérations suivantes:

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

});

C'est IE 10+, Chrome 8+, FF 4+. Voir https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL

Le fichier ne sera téléchargé que sur Chrome, Firefox et Opera. Cela utilise un attribut de téléchargement sur la balise d'ancrage pour forcer le navigateur à le télécharger.

40
JoshBerke

Je connais ce genre de vieux, mais je pense avoir trouvé une solution plus élégante. J'ai eu exactement le même problème. Le problème que j’avais avec les solutions suggérées était qu’elles nécessitaient toutes l’enregistrement du fichier sur le serveur, mais je ne voulais pas enregistrer les fichiers sur le serveur, car cela posait d’autres problèmes (sécurité: le fichier pouvait alors être consulté). utilisateurs non authentifiés, nettoyage: comment et quand vous débarrassez-vous des fichiers). Et comme vous, mes données étaient complexes, des objets JSON imbriqués qu'il serait difficile de mettre dans un formulaire.

Ce que j'ai fait était de créer deux fonctions de serveur. Le premier a validé les données. S'il y avait une erreur, elle serait retournée. Si ce n'était pas une erreur, j'ai renvoyé tous les paramètres sérialisés/encodés sous forme de chaîne base64. Ensuite, sur le client, j'ai un formulaire qui n'a qu'une entrée masquée et qui est publié sur une deuxième fonction serveur. Je règle l'entrée cachée sur la chaîne base64 et soumets le format. La deuxième fonction serveur décode/désérialise les paramètres et génère le fichier. Le formulaire pourrait être soumis à une nouvelle fenêtre ou à un iframe sur la page et le fichier s'ouvrira.

Il y a un peu de travail, et peut-être un peu plus de traitement, mais dans l'ensemble, je me sentais beaucoup mieux avec cette solution.

Le code est en 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);
    }

sur le 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
    }
15
amersk

Il existe un moyen plus simple, créer un formulaire et le poster, cela risquerait de réinitialiser la page si le type de retour mime est quelque chose qu'un navigateur ouvrirait, mais pour csv et ainsi de suite, il est parfait

L'exemple nécessite des traits de soulignement et 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();

Pour des choses comme le HTML, le texte et autres, assurez-vous que le type MIME ressemble à quelque chose comme application/octet-stream

code 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;
9
aqm

En bref, il n'y a pas de moyen plus simple. Vous devez faire une autre demande de serveur pour afficher le fichier PDF. Cependant, il existe peu d'alternatives mais elles ne sont pas parfaites et ne fonctionneront pas sur tous les navigateurs:

  1. Regardez schéma d'URI de données . Si les données binaires sont petites, vous pouvez peut-être utiliser javascript pour ouvrir une fenêtre en transmettant des données dans un URI.
  2. La seule solution Windows/IE serait d'avoir le contrôle .NET ou FileSystemObject pour enregistrer les données sur le système de fichiers local et les ouvrir à partir de là.
8
VinayC

Cela fait longtemps que cette question a été posée, mais j'avais le même défi et je voulais partager ma solution. Il utilise des éléments des autres réponses mais je n'ai pas pu le trouver dans son intégralité. Il n'utilise pas de formulaire ni d'iframe, mais nécessite une paire requête post/get. Au lieu d’enregistrer le fichier entre les requêtes, il enregistre les données de publication. Cela semble être à la fois simple et efficace.

client

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

serveur

[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");
}
7
Frank Rem
$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);
};
6
James McGuigan

Pas tout à fait une réponse à la publication d'origine, mais une solution rapide et délicate pour poster un objet json sur le serveur et générer dynamiquement un téléchargement.

JQuery côté client:

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

..et ensuite décoder la chaîne json au niveau du serveur et définir les en-têtes pour le téléchargement (exemple PHP):

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

Je pense que la meilleure approche consiste à utiliser une combinaison. Votre deuxième approche semble être une solution élégante impliquant des navigateurs.

Donc, en fonction de la façon dont l'appel est effectué. (qu’il s’agisse d’un navigateur ou d’un appel à un service Web), vous pouvez combiner les deux, l’envoi d’une URL au navigateur et l’envoi de données brutes à tout autre client de service Web.

2
naikus

Je suis éveillé depuis deux jours et j'essaie de comprendre comment télécharger un fichier en utilisant jquery avec un appel ajax. Tout le soutien que j'ai obtenu ne pouvait pas aider ma situation jusqu'à ce que j'essaie.

Côté client

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

}

Côté serveur

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

Bonne chance.

1
Otis-iDev

Avec HTML5, vous pouvez simplement créer une ancre et cliquer dessus. Il n'est pas nécessaire de l'ajouter au document en tant qu'enfant.

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

Terminé.

Si vous souhaitez attribuer un nom spécial au téléchargement, transmettez-le simplement dans l'attribut download:

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

Une autre approche, au lieu de sauvegarder le fichier sur le serveur et de le récupérer, consiste à utiliser .NET 4.0+ ObjectCache avec une expiration courte jusqu'à la deuxième action (à ce moment, il peut être vidé définitivement). La raison pour laquelle je souhaite utiliser JQuery Ajax pour effectuer l'appel est que l'appel est asynchrone. Construire mon fichier dynamique PDF prend un peu de temps, et j'affiche une boîte de dialogue déroulante très occupée pendant cette période (cela permet également d'effectuer un autre travail). L'approche consistant à utiliser les données renvoyées dans "success:" pour créer un blob ne fonctionne pas de manière fiable. Cela dépend du contenu du fichier PDF. Il est facilement corrompu par les données de la réponse, si ce n'est pas complètement textuel, ce qui est tout ce que Ajax peut gérer.

0
Wray Smallwood

Je l'ai trouvé quelque part depuis longtemps et cela fonctionne parfaitement!

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
Den Nikitin