441 votes

Gérer le téléchargement de fichiers à partir d'un post ajax

J'ai une application javascript qui envoie des requêtes POST ajax à une certaine URL. La réponse peut être une chaîne JSON ou un fichier (en pièce jointe). Je peux facilement détecter Content-Type et Content-Disposition dans mon appel ajax, mais une fois que j'ai détecté 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 fils similaires ici, mais aucun d'entre eux ne fournit la réponse que je recherche.

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

0 votes

Pour ceux qui ont lu cet article, lisez ce post : stackoverflow.com/questions/20830309/

0 votes

J'ai supprimé votre solution de la question. Vous pouvez la publier dans un message de réponse ci-dessous, mais elle n'a pas sa place dans le message de la question.

600voto

Jonathan Amend Points 4704

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

var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.onload = function () {
    if (this.status === 200) {
        var blob = this.response;
        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, '');
        }

        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.href = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location.href = downloadUrl;
            }

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

Ou si vous utilisez jQuery.ajax :

$.ajax({
    type: "POST",
    url: url,
    data: params,
    xhrFields: {
        responseType: 'blob' // to avoid binary data being mangled on charset conversion
    },
    success: function(blob, 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, '');
        }

        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.href = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location.href = downloadUrl;
            }

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

1 votes

Merci beaucoup ! J'ai dû ajouter 'Content-disposition' à la fois à 'Access-Control-Expose-Headers' et 'Access-Control-Allow-Headers' dans ma réponse HTTP pour que cela fonctionne.

1 votes

Cela ne fonctionne pas lorsque le fichier est plus grand que 500 mb, peut-être devrions-nous utiliser une autre api ?

1 votes

Qu'en est-il de la suppression de l'élément a du DOM dans la partie de nettoyage (et pas seulement de l'URL) ? document.body.removeChild(a);

122voto

jqueryrocks Points 1758

Créez un formulaire, utilisez la méthode POST, soumettez le formulaire - il n'y a pas besoin d'un iframe. Lorsque la page du serveur répond à la demande, écrivez un en-tête de réponse pour le type mime du fichier, et il présentera un dialogue de téléchargement - j'ai fait cela un certain nombre de fois.

Vous voulez le type de contenu de l'application/du téléchargement - il suffit de rechercher comment fournir un téléchargement pour la langue que vous utilisez.

39 votes

Comme indiqué dans la question : "L'utilisation d'un formulaire HTML simple n'est pas non plus une option."

0 votes

Vous pouvez masquer le formulaire et le remplir de manière programmatique - cela ne fonctionne toujours pas ?

14 votes

Non, car l'utilisation d'un POST normal ferait naviguer le navigateur vers l'URL du POST. Je ne veux pas m'éloigner de la page. Je veux exécuter la requête en arrière-plan, traiter la réponse et la présenter au client.

42voto

Naren Yellavula Points 2806

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

" Poster des données JSON au serveur et recevoir un fichier excel. Ce fichier Excel est créé par le serveur et renvoyé comme 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 fait simplement ce qui suit

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

Ici, nous devons définir soigneusement quelques éléments du côté du serveur. J'ai défini quelques en-têtes dans Python Django HttpResponse. Vous devez les définir 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")

Comme je télécharge des xls(excel) ici, j'ai ajusté le contentType à celui ci-dessus. Vous devez le définir en fonction de votre type de fichier. Vous pouvez utiliser cette technique pour télécharger tout type de fichiers.

35voto

Robin van Baalen Points 1177

Quel langage 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 bons en-têtes 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 va pas s'éloigner de la page actuelle.

Il s'agit de définir les bons en-têtes. 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.

Traitement de la réponse côté client

En supposant que vous savez déjà comment faire un appel AJAX, vous exécutez une requête AJAX vers le serveur du côté client. Le serveur génère alors un lien à partir duquel ce fichier peut être téléchargé, par exemple l'URL "forward" vers laquelle vous voulez pointer. Par exemple, le serveur répond par :

{
    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 de fixer le iframe à l'URL que vous venez de recevoir comme ceci (en utilisant jQuery pour faciliter cet exemple) :

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

Si vous avez défini les bons en-têtes comme indiqué ci-dessus, l'iframe forcera l'ouverture d'une boîte de dialogue de téléchargement sans que le navigateur ne s'éloigne de la page actuelle.

Note

Pour répondre à votre question, je pense qu'il est préférable de toujours renvoyer le JSON lorsque vous demandez des choses avec la technologie AJAX. Après avoir reçu la réponse JSON, vous pouvez alors décider côté client de ce que vous voulez en faire. Par exemple, si plus tard vous souhaitez 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 les côtés client et serveur pour le faire.

28voto

Tim Hettler Points 631

Pour ceux qui recherchent une solution du point de vue d'Angular, ceci 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();
});

2 votes

Cela m'a aidé, mais je dois conserver le nom de fichier original. Je vois le nom de fichier dans les en-têtes de réponse sous "Content-Disposition", mais je ne trouve pas cette valeur dans l'objet de réponse dans le code. Réglage de link.download = "" donne un nom de fichier aléatoire, et link.download = null donne lieu à un fichier nommé "null".

0 votes

@Marie, vous pouvez enregistrer le nom du fichier au moment du téléchargement en utilisant la fonction INPUT de l'élément HTMLInputElement.files propriété. Voir Les documents MDN sur l'entrée du fichier pour plus de détails.

0 votes

La taille du Blob est limitée : stackoverflow.com/questions/28307789/

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