web-dev-qa-db-fra.com

Gérer le téléchargement de fichier depuis ajax post

J'ai une application javascript qui envoie des requêtes ajax POST à une certaine URL. La réponse peut être une chaîne JSON ou un fichier (en tant que pièce jointe). Je peux facilement détecter Content-Type et Content-Disposition dans mon appel ajax, mais une fois que je détecte que la réponse contient un fichier, comment puis-je proposer au client de le télécharger? J'ai lu un certain nombre de discussions similaires ici, mais aucune d'elles ne fournit la réponse que je cherche.

S'il vous plaît, s'il vous plaît, s'il vous plaît, ne postez pas de réponses suggérant que je ne devrais pas utiliser ajax pour ceci ou que je devrais rediriger le navigateur, car rien de tout cela n'est une option. L'utilisation d'un formulaire HTML simple n'est également pas une option. Ce dont j'ai besoin, c'est de montrer une boîte de dialogue de téléchargement au client. Est-ce possible de le faire et comment?

MODIFIER:

Apparemment, cela ne peut pas être fait, mais il existe une solution de contournement simple, comme le suggère la réponse acceptée. Pour ceux qui rencontrent ce problème à l'avenir, voici comment je l'ai résolu:

$.ajax({
    type: "POST",
    url: url,
    data: params,
    success: function(response, status, request) {
        var disp = request.getResponseHeader('Content-Disposition');
        if (disp && disp.search('attachment') != -1) {
            var form = $('<form method="POST" action="' + url + '">');
            $.each(params, function(k, v) {
                form.append($('<input type="hidden" name="' + k +
                        '" value="' + v + '">'));
            });
            $('body').append(form);
            form.submit();
        }
    }
});

Donc, fondamentalement, il suffit de générer un formulaire HTML avec les mêmes paramètres que ceux utilisés dans la demande AJAX et de le soumettre.

367
Pavle Predic

Créez un formulaire, utilisez la méthode POST, soumettez-le - aucun iframe n'est nécessaire. Lorsque la page du serveur répond à la demande, écrivez un en-tête de réponse pour le type mime du fichier et une boîte de dialogue de téléchargement s'affichera. Je l'ai déjà fait plusieurs fois.

Vous voulez un type de contenu d’application/de téléchargement - il vous suffit de chercher comment fournir un téléchargement pour la langue que vous utilisez.

108
user571545

N'abandonnez pas si vite, car cela peut être fait (dans les navigateurs modernes) en utilisant des parties de FileAPI:

Edit 2017-09-28: Mise à jour pour utiliser le constructeur de fichiers lorsqu'il est disponible afin qu'il fonctionne dans Safari> = 10.1.

Éditer 2015-10-16: jQuery ajax ne peut pas gérer correctement les réponses binaires (impossible de définir le responseType). Il est donc préférable d'utiliser un appel XMLHttpRequest simple.

var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
    if (this.status === 200) {
        var filename = "";
        var disposition = xhr.getResponseHeader('Content-Disposition');
        if (disposition && disposition.indexOf('attachment') !== -1) {
            var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
            var matches = filenameRegex.exec(disposition);
            if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
        }
        var type = xhr.getResponseHeader('Content-Type');

        var blob = typeof File === 'function'
            ? new File([this.response], filename, { type: type })
            : new Blob([this.response], { type: type });
        if (typeof window.navigator.msSaveBlob !== 'undefined') {
            // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
            window.navigator.msSaveBlob(blob, filename);
        } else {
            var URL = window.URL || window.webkitURL;
            var downloadUrl = URL.createObjectURL(blob);

            if (filename) {
                // use HTML5 a[download] attribute to specify filename
                var a = document.createElement("a");
                // safari doesn't support this yet
                if (typeof a.download === 'undefined') {
                    window.location = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location = downloadUrl;
            }

            setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
        }
    }
};
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send($.param(params));

Voici l'ancienne version utilisant jQuery.ajax. Il est possible que les données binaires soient modifiées lorsque la réponse est convertie en une chaîne de caractères quelconque.

$.ajax({
    type: "POST",
    url: url,
    data: params,
    success: function(response, status, xhr) {
        // check for a filename
        var filename = "";
        var disposition = xhr.getResponseHeader('Content-Disposition');
        if (disposition && disposition.indexOf('attachment') !== -1) {
            var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
            var matches = filenameRegex.exec(disposition);
            if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
        }

        var type = xhr.getResponseHeader('Content-Type');
        var blob = new Blob([response], { type: type });

        if (typeof window.navigator.msSaveBlob !== 'undefined') {
            // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
            window.navigator.msSaveBlob(blob, filename);
        } else {
            var URL = window.URL || window.webkitURL;
            var downloadUrl = URL.createObjectURL(blob);

            if (filename) {
                // use HTML5 a[download] attribute to specify filename
                var a = document.createElement("a");
                // safari doesn't support this yet
                if (typeof a.download === 'undefined') {
                    window.location = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location = downloadUrl;
            }

            setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
        }
    }
});
495
Jonathan Amend

J'ai fait face au même problème et l'ai résolu avec succès. Mon cas d'utilisation est le suivant.

"Publiez des données JSON sur le serveur et recevez un fichier Excel. Ce fichier Excel est créé par le serveur et renvoyé en réponse au client. Téléchargez cette réponse sous forme de fichier avec un nom personnalisé dans le navigateur"

$("#my-button").on("click", function(){

// Data to post
data = {
    ids: [1, 2, 3, 4, 5]
};

// Use XMLHttpRequest instead of Jquery $ajax
xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
    var a;
    if (xhttp.readyState === 4 && xhttp.status === 200) {
        // Trick for making downloadable link
        a = document.createElement('a');
        a.href = window.URL.createObjectURL(xhttp.response);
        // Give filename you wish to download
        a.download = "test-file.xls";
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
    }
};
// Post data to URL which handles post request
xhttp.open("POST", excelDownloadUrl);
xhttp.setRequestHeader("Content-Type", "application/json");
// You should set responseType as blob for binary responses
xhttp.responseType = 'blob';
xhttp.send(JSON.stringify(data));
});

L'extrait ci-dessus ne fait que suivre

  • Publication d'un tableau au format JSON sur le serveur à l'aide de XMLHttpRequest.
  • Après avoir récupéré le contenu sous forme de blob (binaire), nous créons une URL téléchargeable et l'attachons à un lien "a" invisible, puis cliquez dessus.

Ici, nous devons définir soigneusement quelques éléments côté serveur. J'ai défini quelques en-têtes dans Python Django HttpResponse. Vous devez les configurer en conséquence si vous utilisez d'autres langages de programmation.

# In python Django code
response = HttpResponse(file_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")

Depuis que je télécharge xls (Excel) ici, j’ai ajusté contentType à la valeur précédente. Vous devez le définir en fonction de votre type de fichier. Vous pouvez utiliser cette technique pour télécharger tout type de fichier.

32
Naren Yellavula

Quelle langue côté serveur utilisez-vous? Dans mon application, je peux facilement télécharger un fichier à partir d'un appel AJAX en définissant les en-têtes appropriés dans la réponse de PHP:

Définition des en-têtes côté serveur

header("HTTP/1.1 200 OK");
header("Pragma: public");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");

// The optional second 'replace' parameter indicates whether the header
// should replace a previous similar header, or add a second header of
// the same type. By default it will replace, but if you pass in FALSE
// as the second argument you can force multiple headers of the same type.
header("Cache-Control: private", false);

header("Content-type: " . $mimeType);

// $strFileName is, of course, the filename of the file being downloaded. 
// This won't have to be the same name as the actual file.
header("Content-Disposition: attachment; filename=\"{$strFileName}\""); 

header("Content-Transfer-Encoding: binary");
header("Content-Length: " . mb_strlen($strFile));

// $strFile is a binary representation of the file that is being downloaded.
echo $strFile;

Cela va en fait "rediriger" le navigateur vers cette page de téléchargement, mais comme @ahren l'a déjà dit dans son commentaire, il ne quittera pas la page actuelle.

Il s’agit de définir les bons en-têtes, donc je suis sûr que vous trouverez une solution adaptée au langage côté serveur que vous utilisez si ce n’est pas PHP.

Gestion du côté client de la réponse

En supposant que vous sachiez déjà comment passer un appel AJAX, côté client, vous exécutez une demande AJAX au serveur. Le serveur génère ensuite un lien à partir duquel ce fichier peut être téléchargé, par exemple. l'URL de "transfert" vers laquelle vous souhaitez pointer. Par exemple, le serveur répond avec:

{
    status: 1, // ok
    // unique one-time download token, not required of course
    message: 'http://yourwebsite.com/getdownload/ska08912dsa'
}

Lors du traitement de la réponse, vous injectez un iframe dans votre corps et définissez le code SRC de iframe sur l'URL que vous venez de recevoir (en utilisant jQuery pour simplifier cet exemple):

$("body").append("<iframe src='" + data.message +
  "' style='display: none;' ></iframe>");

Si vous avez défini les en-têtes appropriés, comme indiqué ci-dessus, l'iframe forcera une boîte de dialogue de téléchargement sans quitter le navigateur de la page en cours.

Remarque

Supplément supplémentaire par rapport à votre question. Je pense qu'il est préférable de toujours renvoyer JSON lorsque vous demandez des éléments avec la technologie AJAX. Une fois que vous avez reçu la réponse JSON, vous pouvez ensuite décider du côté client quoi en faire. Vous voudrez peut-être par exemple ultérieurement que l'utilisateur clique sur un lien de téléchargement vers l'URL au lieu de forcer le téléchargement directement. Dans votre configuration actuelle, vous devrez mettre à jour le client et le serveur pour le faire.

31
Robin van Baalen

Pour ceux qui recherchent une solution d'un point de vue Angular, cela a fonctionné pour moi:

$http.post(
  'url',
  {},
  {responseType: 'arraybuffer'}
).then(function (response) {
  var headers = response.headers();
  var blob = new Blob([response.data],{type:headers['content-type']});
  var link = document.createElement('a');
  link.href = window.URL.createObjectURL(blob);
  link.download = "Filename";
  link.click();
});
22
Tim Hettler

Voici comment j'ai obtenu ce travail https://stackoverflow.com/a/27563953/2845977

$.ajax({
  url: '<URL_TO_FILE>',
  success: function(data) {
    var blob=new Blob([data]);
    var link=document.createElement('a');
    link.href=window.URL.createObjectURL(blob);
    link.download="<FILENAME_TO_SAVE_WITH_EXTENSION>";
    link.click();
  }
});

Réponse mise à jour en utilisant download.js

$.ajax({
  url: '<URL_TO_FILE>',
  success: download.bind(true, "<FILENAME_TO_SAVE_WITH_EXTENSION>", "<FILE_MIME_TYPE>")
});
14
Mayur Padshala

Je vois que vous avez déjà trouvé une solution, mais je voulais simplement ajouter quelques informations qui pourraient aider quelqu'un qui essaie de faire la même chose avec de grosses requêtes POST.

J'ai eu le même problème il y a quelques semaines. En effet, il n'est pas possible de réaliser un téléchargement "propre" via AJAX, le groupe de filaments a créé un plugin jQuery qui fonctionne exactement comme vous l'avez déjà découvert, il s'appelle - Téléchargement de fichier jQuery Toutefois, cette technique présente des inconvénients.

Si vous envoyez de grandes demandes via AJAX (fichiers + 1 Mo, par exemple), la réactivité en sera affectée. Dans les connexions Internet lentes, vous devrez attendre beaucoup jusqu'à l'envoi de la demande et attendre le téléchargement du fichier. Ce n'est pas comme un instant "click" => "popup" => "download start". Cela ressemble plus à "click" => "attendre que les données soient envoyées" => "wait for response" => "démarrer le téléchargement", ce qui fait apparaître le fichier doublant sa taille car vous devrez attendre que la demande soit envoyée. via AJAX et le récupérer sous forme de fichier téléchargeable.

Si vous travaillez avec des fichiers de petite taille <1 Mo, vous ne le remarquerez pas. Mais comme je l’ai découvert dans ma propre application, pour des fichiers de plus grande taille, c’est presque insupportable.

Mon application autorise les utilisateurs à exporter des images générées dynamiquement. Ces images sont envoyées au serveur par le biais de POST requêtes au format base64 (elles constituent le seul moyen possible), puis traitées et renvoyées aux utilisateurs sous la forme de .png , Les fichiers .jpg, les chaînes base64 pour les images + 1 Mo sont énormes, ce qui oblige les utilisateurs à attendre plus que nécessaire pour que le fichier puisse commencer à se télécharger. Dans les connexions Internet lentes, cela peut être très ennuyant.

Ma solution pour cela consistait à écrire temporairement le fichier sur le serveur. Une fois prêt, il créait dynamiquement un lien vers le fichier sous la forme d'un bouton qui change entre les états "Please wait ..." et "Download", et en même temps. temps, imprimez l’image base64 dans une fenêtre contextuelle afin que les utilisateurs puissent "cliquer avec le bouton droit" et l’enregistrer. Cela rend tout le temps d'attente plus supportable pour les utilisateurs et accélère les choses.

Mise à jour du 30 septembre 2014:

Des mois ont passé depuis que j'ai posté ceci, enfin, j'ai trouvé une meilleure approche pour accélérer les choses lorsque je travaille avec de grandes chaînes base64. Je stocke maintenant les chaînes base64 dans la base de données (à l'aide de champs longtext ou longblog), puis je passe son ID d'enregistrement via le téléchargement de fichier jQuery, enfin sur le fichier script de téléchargement, j'interroge la base de données à l'aide de cet ID pour extraire la chaîne base64 et la transmettre. la fonction de téléchargement.

Exemple de script de téléchargement:

<?php
// Record ID
$downloadID = (int)$_POST['id'];
// Query Data (this example uses CodeIgniter)
$data       = $CI->MyQueries->GetDownload( $downloadID );
// base64 tags are replaced by [removed], so we strip them out
$base64     = base64_decode( preg_replace('#\[removed\]#', '', $data[0]->image) );
// This example is for base64 images
$imgsize    = getimagesize( $base64 );
// Set content headers
header('Content-Disposition: attachment; filename="my-file.png"');
header('Content-type: '.$imgsize['mime']);
// Force download
echo $base64;
?>

Je sais que cela va bien au-delà de ce que le PO a demandé, mais j’ai pensé qu’il serait bien de mettre à jour ma réponse avec mes conclusions. Lorsque je cherchais des solutions à mon problème, je lisais beaucoup de "Télécharger de AJAX POST données" discussions qui ne m'ont pas donné la réponse que je cherchais, j'espère que cette information aidera quelqu'un qui cherche à réaliser quelque chose comme ça.

12
José SAYAGO

Je souhaite signaler certaines difficultés rencontrées lors de l’utilisation de la technique dans la réponse acceptée, c’est-à-dire lors de l’utilisation d’un formulaire:

  1. Vous ne pouvez pas définir les en-têtes sur la demande. Si votre schéma d'authentification implique des en-têtes, un jeton Json-Web-Token passé dans l'en-tête Authorization, vous devrez trouver un autre moyen de l'envoyer, par exemple en tant que paramètre de requête.

  2. Vous ne pouvez pas vraiment dire quand la demande est terminée. Eh bien, vous pouvez utiliser un cookie qui est défini sur la réponse, comme indiqué par jquery.fileDownload , mais c’est loin de parfait. Cela ne fonctionnera pas pour les demandes simultanées et se rompra si une réponse n'arrive jamais.

  3. Si le serveur répond avec une erreur, l'utilisateur sera redirigé vers la page d'erreur.

  4. Vous ne pouvez utiliser que les types de contenu pris en charge par un formulaire . Ce qui signifie que vous ne pouvez pas utiliser JSON.

J'ai fini par utiliser la méthode consistant à enregistrer le fichier sur S3 et à envoyer une URL pré-signée pour obtenir le fichier.

5
tepez

Voici ma solution en utilisant une forme cachée temporaire.

//Create an hidden form
var form = $('<form>', {'method': 'POST', 'action': this.href}).hide();

//Add params
var params = { ...your params... };
$.each(params, function (k, v) {
    form.append($('<input>', {'type': 'hidden', 'name': k, 'value': v}));
});

//Make it part of the document and submit
$('body').append(form);
form.submit();

//Clean up
form.remove();

Notez que j'utilise massivement JQuery mais vous pouvez faire la même chose avec JS natif.

3
Ludovic Martin

C'est une question de 3 ans, mais j'ai eu le même problème aujourd'hui. J'ai jeté un coup d'œil à votre solution modifiée, mais je pense qu'elle peut sacrifier la performance car elle doit faire une double demande. Donc, si quelqu'un a besoin d'une autre solution qui n'implique pas d'appeler le service deux fois, voici comment je l'ai fait:

<form id="export-csv-form" method="POST" action="/the/path/to/file">
    <input type="hidden" name="anyValueToPassTheServer" value="">
</form>

Ce formulaire est juste utilisé pour appeler le service et éviter d'utiliser window.location (). Après cela, il vous suffit simplement de soumettre un formulaire à partir de jquery pour appeler le service et récupérer le fichier. C'est assez simple, mais de cette façon, vous pouvez effectuer un téléchargement en utilisant POST. Je pense maintenant que cela pourrait être plus facile si le service que vous appelez est un GET, mais ce n'est pas mon cas.

3
Jairo Miranda

Comme d'autres l'ont indiqué, vous pouvez créer et soumettre un formulaire à télécharger via une demande POST. Cependant, vous n'avez pas à le faire manuellement.

Une bibliothèque très simple pour faire exactement cela est jquery.redirect . Il fournit une API similaire à la méthode standard jQuery.post:

$.redirect(url, [values, [method, [target]]])
2
KurtPreston

voir: http://www.henryalgus.com/reading-binary-files-using-jquery-ajax/ il retournera un blob en réponse, qui pourra ensuite être mis dans filesaver

1
Samantha Adrichem

J'ai utilisé ceci FileSaver.js . Dans mon cas avec les fichiers csv, j’ai fait ceci (en coffescript):

  $.ajax
    url: "url-to-server"
    data: "data-to-send"
    success: (csvData)->
      blob = new Blob([csvData], { type: 'text/csv' })
      saveAs(blob, "filename.csv")

Je pense que pour les cas les plus compliqués, les données doivent être traitées correctement. Sous le capot, FileSaver.js implémente la même approche que la réponse de Jonathan Amend .

1
Armando

Pour que Jonathan Amendsréponse travaille dans Edge, j'ai apporté les modifications suivantes:

var blob = typeof File === 'function'
    ? new File([this.response], filename, { type: type })
    : new Blob([this.response], { type: type });

pour ça

var f = typeof File+"";
var blob = f === 'function' && Modernizr.fileapi
    ? new File([this.response], filename, { type: type })
    : new Blob([this.response], { type: type });

J'aurais plutôt posté cela comme un commentaire mais je n'ai pas assez de réputation pour ça

1
fstrandner

il existe une autre solution pour télécharger une page Web en ajax. Mais je me réfère à une page qui doit d’abord être traitée puis téléchargée.

Vous devez d’abord séparer le traitement de la page du téléchargement des résultats.

1) Seuls les calculs de page sont effectués dans l'appel ajax.

 $. post ("CalculusPage.php", {calculusFunction: true, ID: 29, data1: "a", data2: "b"}, 
 
 fonction (data , statut) 
 {
 if (status == "succès") 
 {
/* 2) Dans la réponse, la page qui utilise les calculs précédents est téléchargée . Par exemple, il peut s'agir d'une page imprimant les résultats d'une table calculée dans l'appel ajax. */
 window.location.href = DownloadPage.php + "? ID =" + 29; 
} 
} 
); 
 
 // Par exemple: dans CalculusPage.php 
 
 If (! Empty ($ _ POST ["calculusFunction"])) 
 {
 $ ID = $ _POST ["ID"]; 
 
 $ Query = "INSERT INTO ExamplePage (data1, data2) VALEURS ('". $ _ POST ["data1"]. "'," ". $ _POST ["data2"]. "') WHERE id =". $ ID; 
 ... 
} 
 
 // Par exemple: dans le DownloadPage.php 
 
 $ ID = $ _GET ["ID"]; 
 
 $ Sede = "SELECT * FROM ExemplePage WHERE id =". $ ID; 
 ... 
 
 $ filename = "Export_Data.xls"; 
 en-tête ("Content-Type: application/vnd.ms-Excel"); 
 en-tête ("Content-Disposition: inline; nomfichier = $ nomfichier"); 
 
 ... 

J'espère que cette solution pourra être utile pour beaucoup, comme pour moi.

0
netluke

Voici ma solution, issue de différentes sources: Implémentation côté serveur:

    String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
    // Set headers
    response.setHeader("content-disposition", "attachment; filename =" + fileName);
    response.setContentType(contentType);
    // Copy file to output stream
    ServletOutputStream servletOutputStream = response.getOutputStream();
    try (InputStream inputStream = new FileInputStream(file)) {
        IOUtils.copy(inputStream, servletOutputStream);
    } finally {
        servletOutputStream.flush();
        Utils.closeQuitely(servletOutputStream);
        fileToDownload = null;
    }

Implémentation côté client (en utilisant jquery):

$.ajax({
type: 'POST',
contentType: 'application/json',
    url: <download file url>,
    data: JSON.stringify(postObject),
    error: function(XMLHttpRequest, textStatus, errorThrown) {
        alert(errorThrown);
    },
    success: function(message, textStatus, response) {
       var header = response.getResponseHeader('Content-Disposition');
       var fileName = header.split("=")[1];
       var blob = new Blob([message]);
       var link = document.createElement('a');
       link.href = window.URL.createObjectURL(blob);
       link.download = fileName;
       link.click();
    }
});   
0
Dvs Prajapati