271 votes

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

J'ai une application web à une seule page basée sur Jquery. Elle communique avec un service web RESTful via des appels AJAX.

J'essaie d'accomplir ce qui suit :

  1. Soumettre un POST qui contient des données JSON à une url REST.
  2. Si la demande spécifie une réponse JSON, alors JSON est retourné.
  3. Si la demande spécifie une réponse PDF/XLS/etc, alors un binaire téléchargeable est renvoyé.

J'ai 1 et 2 qui fonctionnent maintenant, et l'application jquery du client affiche les données retournées dans la page web en créant des éléments DOM basés sur les données JSON. Le point 3 fonctionne également du point de vue du service web, ce qui signifie qu'il créera et renverra un fichier binaire si les paramètres JSON sont corrects. Mais je ne suis pas sûr de la meilleure façon de traiter le point 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 pour 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 JSON comprenant une URL vers le fichier. Ensuite, émettez 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 cela signifie que je devrais faire plus d'un appel au serveur, et que mon serveur devrait créer 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 d'y parvenir. Des idées ?


EDIT : Après avoir examiné la documentation relative à $.ajax, je constate que le type de données de la réponse ne peut être qu'un des éléments suivants xml, html, script, json, jsonp, text Je suppose donc qu'il n'y a aucun moyen de télécharger directement un fichier à l'aide d'une requête ajax, à moins que je n'intègre le fichier binaire en utilisant le schéma URI des données comme le suggère la réponse de @VinayC (ce que je ne souhaite pas faire).

Donc je suppose que mes options sont :

  1. Ne pas utiliser l'ajax, mais plutôt soumettre un formulaire et intégrer mes données JSON dans les valeurs du formulaire. Il faudrait probablement s'amuser avec des iframes cachées et autres.

  2. Ne pas utiliser ajax et convertir mes données JSON en une chaîne de requête pour construire une requête GET standard et définir window.location.href à cette URL. Vous devrez peut-être utiliser event.preventDefault() dans mon gestionnaire de clics pour empêcher le navigateur de changer l'URL de l'application.

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

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

Y a-t-il d'autres moyens d'y parvenir ? Y a-t-il des avantages ou des inconvénients à ces méthodes dont je devrais être conscient ?

0 votes

7 votes

Celui-ci était "un peu" plus tôt, alors en quoi est-ce un doublon ?

1 votes

url = 'http://localhost/file.php?file='+ $('input').val(); window.open(url); Le moyen le plus simple d'obtenir les mêmes résultats. Mettez l'en-tête dans le fichier php. Pas besoin d'envoyer des requêtes ajax ou get.

172voto

SamStephens Points 2913

letronje La solution de l'auteur ne fonctionne que pour des pages très simples. document.body.innerHTML += prend le texte HTML du corps, ajoute le HTML de l'iframe, et définit le innerHTML de la page à cette chaîne. Cela effacera toutes les liaisons d'événements de 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 qu'il fait en réalité : il envoie un message à /create_binary_file.php avec les données contenues dans la variable postData ; si ce message se termine avec succès, il ajoute une nouvelle iframe au corps de la page. L'hypothèse est que la réponse de /create_binary_file.php comprendra une valeur 'url', qui est l'URL à partir de laquelle le fichier PDF/XLS/etc généré peut être téléchargé. L'ajout d'un iframe à la page qui fait référence à cette URL permettra au navigateur d'inviter l'utilisateur à télécharger le fichier, à condition que le serveur Web ait la configuration de type mime appropriée.

1 votes

Je suis d'accord, c'est mieux que d'utiliser += et je mettrai à jour ma demande en conséquence. Merci !

3 votes

J'aime ce concept, mais Chrome affiche ce message dans la console : Resource interpreted as Document but transferred with MIME type application/pdf . Ensuite, il me prévient également que le fichier peut être dangereux. Si je visite directement le retData.url, aucun avertissement ni problème.

0 votes

En regardant sur Internet, il semble que vous pouvez avoir des problèmes avec les PDF dans les iFrames si votre serveur ne définit pas correctement Content-Type et Content-Disposition. Je n'ai pas utilisé cette solution moi-même, j'ai juste clarifié une solution précédente, donc je n'ai pas d'autres conseils à donner.

61voto

letronje Points 3228

Une fois le fichier binaire généré sur le serveur, en supposant qu'il existe une URL accessible au public pour le fichier généré, un iframe caché peut être utilisé pour effectuer le travail sans utiliser de redirection.

Voici comment procéder :

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

60voto

JoshBerke Points 34238

Je me suis amusé avec une autre option qui utilise des blobs. J'ai réussi à télécharger des documents texte, et j'ai téléchargé des PDF (mais ils sont corrompus).

En utilisant l'API blob, vous serez en mesure de 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();

});

C'est 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 que dans Chrome, Firefox et Opera. Cette méthode utilise un attribut de téléchargement sur la balise d'ancrage pour obliger le navigateur à le télécharger.

1 votes

Pas de support pour l'attribut de téléchargement sur IE et Safari :( ( ( caniuse.com/#feat=download ). Vous pouvez ouvrir une nouvelle fenêtre et y placer du contenu, mais je ne suis pas sûr pour un PDF ou un Excel...

3 votes

Vous devez libérer la mémoire du navigateur après avoir appelé click : window.URL.revokeObjectURL(link.href);

0 votes

@JoshBerke C'est vieux mais ça marche chez moi. Comment puis-je faire en sorte qu'il ouvre mon répertoire immédiatement pour l'enregistrer plutôt que d'enregistrer dans mon navigateur ? Il y a aussi un moyen de déterminer le nom du fichier entrant ?

17voto

amersk Points 61

Je sais que c'est un peu vieux, mais je pense avoir trouvé une solution plus élégante. J'avais exactement le même problème. Le problème que j'avais avec les solutions proposées était qu'elles nécessitaient toutes que le fichier soit sauvegardé sur le serveur, mais je ne voulais pas sauvegarder les fichiers sur le serveur, car cela introduisait d'autres problèmes (sécurité : le fichier pourrait alors être accessible par des utilisateurs non authentifiés, nettoyage : comment et quand se débarrasser des fichiers). Et comme vous, mes données étaient des objets JSON complexes et imbriqués qu'il serait difficile de mettre dans un formulaire.

Ce que j'ai fait, c'est créer deux fonctions de serveur. La première validait les données. S'il y avait une erreur, elle était renvoyée. S'il n'y avait pas d'erreur, je renvoyais 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 seule entrée cachée et qui est envoyé à une deuxième fonction du serveur. Je donne à l'entrée cachée la valeur de la chaîne base64 et je soumets le formulaire. La deuxième fonction serveur décode/désérialise les paramètres et génère le fichier. Le formulaire peut être soumis à une nouvelle fenêtre ou à un iframe sur la page et le fichier s'ouvrira.

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
    }

1 votes

Je n'ai pas encore bien examiné cette solution, mais il convient de noter que rien dans la solution utilisant create_binary_file.php n'exige que les fichiers soient enregistrés sur le disque. Il serait tout à fait possible de faire en sorte que create_binary_file.php génère des fichiers binaires en mémoire.

8voto

VinayC Points 23947

En bref, il n'y a pas de méthode plus simple. Vous devez faire une autre requête au serveur pour afficher le fichier PDF. Bien qu'il existe quelques alternatives, elles ne sont pas parfaites et ne fonctionnent pas sur tous les navigateurs :

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

0 votes

Merci, je ne connaissais pas le schéma Data URI. Il semble que ce soit le seul moyen de le faire en une seule requête. Mais ce n'est pas vraiment la direction que je souhaite prendre.

0 votes

En raison des exigences du client, j'ai fini par résoudre ce problème en utilisant les en-têtes du serveur dans la réponse ( Content-Disposition: attachment;filename="file.txt" ), mais je veux vraiment essayer cette approche la prochaine fois. J'avais utilisé URI pour les vignettes, mais il ne m'est jamais venu à l'esprit de l'utiliser pour les téléchargements - merci de partager cette pépite.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X