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

j'ai une webapp à une page basée sur jquery. Il communique avec un service Web reposant via des appels AJAX.

j'essaie d'accomplir ce qui suit:

  1. Soumettre un POST qui contient les données JSON à un RESTE de l'url.
  2. si la requête spécifie une réponse JSON, alors JSON est retourné.
  3. si la requête spécifie une réponse PDF/XLS/etc, alors un binaire téléchargeable est retourné.

j'ai 1 & 2 de travail maintenant, et le client jquery application affiche les données renvoyées dans la page web en créant des éléments du DOM basés sur les données JSON. J'ai aussi #3 qui fonctionne du point de vue du service web, ce qui signifie qu'il va créer et retourner un fichier binaire si on lui donne les bons paramètres JSON. Mais je ne suis pas sûr de la meilleure façon de traiter le numéro 3 dans le code client javascript.

est-il possible de récupérer un fichier téléchargeable d'un appel ajax comme celui-ci? Comment est-ce que je peux télécharger et sauvegarder le fichier avec le navigateur?

$.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 est de générer le PDF et de le stocker sur le serveur et de retourner JSON qui inclut une URL vers le fichier. Ensuite, lancez un autre appel dans le gestionnaire de succès ajax pour faire quelque chose comme ce qui suit:

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

mais faire cela signifie que je devrais faire plus d'un appel à la serveur, et mon serveur aurait besoin de construire des fichiers téléchargeables, les stocker quelque part, puis nettoyer périodiquement cette zone de stockage.

il doit y avoir un moyen plus simple pour accomplir ceci. Des idées?


modifier: après avoir examiné les docs pour $.ajax, je vois que le type de données de réponse ne peut être qu'un de xml, html, script, json, jsonp, text , donc je devine qu'il n'y a aucun moyen de télécharger directement un fichier en utilisant une requête ajax, à moins que je n'intègre le fichier binaire dans utiliser le schéma D'URI des données comme suggéré dans la réponse @VinayC (ce qui n'est pas quelque chose que je veux faire).

donc je suppose que mes options sont:

  1. N'utilisez pas ajax et soumettez plutôt un formulaire post et intégrez mes données JSON dans les valeurs du formulaire. Serait probablement besoin de retoucher avec des iframes.

  2. N'utilisez pas ajax et convertissez plutôt mes données JSON en une chaîne de requête pour construire un GET standard demande et de l'ensemble de la fenêtre.emplacement.href cette adresse URL. Pouvez avoir besoin d'utiliser de l'événement.preventDefault () dans mon gestionnaire de clic pour empêcher le navigateur de changer l'URL de l'application.

  3. utilisez mon autre idée ci-dessus, mais améliorée avec les suggestions de la réponse @naikus. Soumettez la requête AJAX avec un paramètre qui permet à web-service de savoir qu'elle est appelée via un appel ajax. Si le service web est appelé à partir d'un appel ajax, il suffit de retourner JSON avec une URL à la ressources. Si la ressource est appelée directement, puis retourner le fichier binaire.

plus j'y pense, plus j'aime la dernière option. De cette façon, je peux récupérer des informations sur la requête (temps de génération, Taille du fichier, messages d'erreur, etc.) et je peux agir sur cette information avant de commencer le téléchargement. L'inconvénient est la gestion de fichiers supplémentaires sur le serveur.

D'autres moyens d'accomplir ceci? Les avantages/inconvénients à ces méthodes que je devrais connaître?

219
demandé sur cezar 2010-08-17 09:36:46

12 réponses

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 place l'innerHTML de la page à cette chaîne. Cela effacera toutes les liaisons d'événements sur votre page, entre autres choses. 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>");
}); 

ce qui n': effectuer un post / create_binary_file.php avec les données dans la variable postData; si ce post remplit avec succès, ajouter 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 est L'URL à partir de laquelle le fichier PDF/XLS/etc généré peut être téléchargé. Ajouter une iframe à la page qui renvoie à cette URL fera en sorte que le navigateur encouragera l'utilisateur à télécharger le fichier, en supposant que le serveur web a le type mime approprié configuration.

162
répondu SamStephens 2015-11-06 18:39:02

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

en utilisant l'API blob vous pourrez faire ce qui suit:

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

});

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

il ne téléchargera le fichier dans Chrome, Firefox et Opera. Cela utilise un attribut de téléchargement sur l'étiquette d'ancrage pour forcer le navigateur à le télécharger.

31
répondu JoshBerke 2017-05-12 15:14:37

je connais ce genre de vieux, mais je pense que j'ai trouvé une solution plus élégante. J'ai eu exactement le même problème. Le problème que j'ai avec les solutions suggérées que tous les fichiers sont sauvegardés sur le serveur, mais je ne voulais pas enregistrer les fichiers sur le serveur, car il introduit d'autres problèmes (sécurité: le fichier peut alors être consulté par les utilisateurs non authentifiés, de nettoyage: quand et comment voulez-vous vous débarrasser des fichiers). Et comme vous, mes données complexes, imbriquées JSON objets qui seraient difficiles à mettre dans une forme.

ce que j'ai fait, c'est 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 retourné tous les paramètres serialized/encoded comme une chaîne de base64. Puis, sur le client, j'ai une forme qui n'a qu'une entrée cachée et des messages à une fonction de serveur de seconde. J'ai placé l'entrée cachée dans la chaîne base64 et j'ai soumis le format. La deuxième fonction de serveur décode/désérialise les paramètres et génère le fichier. La forme peut soumettre à une nouvelle fenêtre ou un iframe sur la page et le fichier va s'ouvrir.

il y a un peu plus de travail, et peut-être un peu plus de traitement, mais dans l'ensemble, je me suis senti 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
répondu amersk 2013-05-08 08:47:41

il y a une façon plus simple, créer un formulaire et le poster, cela court le risque de réinitialiser la page si le type mime de retour est quelque chose qu'un navigateur ouvrirait, mais pour csv et tel il est parfait

exemple nécessite underscore 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 les choses comme html, texte et tel, assurez-vous que le mimetype est 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
répondu aqm 2014-03-20 14:02:05

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

  1. Regardez données de schéma d'URI . Si les données binaires sont petites alors vous pouvez peut-être utiliser javascript pour ouvrir la fenêtre passant des données dans URI.
  2. Windows / IE seule solution serait D'avoir .NET control ou FileSystemObject enregistrer les données sur le système de fichiers local et l'ouvrir de là.
8
répondu VinayC 2010-08-17 06:17:07

Cela fait longtemps que cette question n'a pas été posée, mais j'ai eu le même défi et je veux partager ma solution. Il utilise des éléments tirés des autres réponses, mais je n'ai pas pu le trouver dans son intégralité. Il n'utilise pas un formulaire ou une iframe mais il nécessite une paire de requêtes post/get. Au lieu de sauvegarder le fichier entre les requêtes, il sauvegarde les données post. Elle semble à 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
répondu Frank Rem 2015-04-08 07:46:10
$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
répondu James McGuigan 2016-04-07 20:25:30

je pense que la meilleure approche est d'utiliser une combinaison, Votre deuxième approche semble être une solution élégante où les navigateurs sont impliqués.

dépend donc de la façon dont l'appel est fait. (qu'il s'agisse d'un navigateur ou d'un appel de service web) vous pouvez utiliser une combinaison des deux, avec l'envoi D'une URL au navigateur et l'envoi de données brutes à tout autre client de service web.

2
répondu naikus 2010-08-17 05:43:49

N'est pas entièrement une réponse au message original, mais une solution rapide et sale pour poster un objet json sur le serveur et générer dynamiquement un téléchargement.

côté Client 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();
}

..et ensuite décoder la chaîne json au niveau du serverside 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');
2
répondu ralftar 2015-10-16 21:42:36

je suis éveillé depuis deux jours maintenant essayer de comprendre comment télécharger un fichier en utilisant jquery avec appel ajax. Tout le soutien que j'ai reçu ne pouvait pas aider ma situation jusqu'à ce que j'essaie ça.

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
répondu Otis-iDev 2015-09-01 03:02:00

une autre approche au lieu d'enregistrer le fichier sur le serveur et de le récupérer, est D'utiliser .NET 4.0+ ObjectCache avec une courte expiration jusqu'à la seconde Action (à laquelle il peut être définitivement jeté). La raison pour laquelle je veux utiliser JQuery Ajax pour faire l'appel, c'est qu'il est asynchrone. La construction de mon fichier PDF dynamique prend beaucoup de temps, et j'affiche un dialogue spinner occupé pendant ce temps (il permet aussi d'effectuer d'autres travaux). L'approche de l'utilisation des données contenues dans le "succès": créer un Blob ne fonctionne pas de manière fiable. Cela dépend du contenu du fichier PDF. Il est facilement corrompu par des données dans la réponse, si elle n'est pas entièrement textuelle qui est tout ce que Ajax peut gérer.

0
répondu Wray Smallwood 2015-05-19 20:14:11

avec HTML5, vous pouvez créer une ancre et cliquer dessus. Il n'est pas nécessaire d'ajouter le document comme un enfant.

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

terminé.

si vous voulez avoir un nom spécial pour le téléchargement, passez-le dans l'attribut download :

const a = document.createElement('a');
a.download = 'my-special-name.pdf';
a.href = urlForPdfFile;
a.click();
0
répondu rewritten 2018-04-19 10:18:27