Manejar la descarga de archivos desde la publicación ajax

Resuelto Pavle Predic asked hace 11 años • 22 respuestas

Tengo una aplicación de JavaScript que envía solicitudes POST ajax a una URL determinada. La respuesta puede ser una cadena JSON o un archivo (como archivo adjunto). Puedo detectar fácilmente el tipo de contenido y la disposición del contenido en mi llamada ajax, pero una vez que detecto que la respuesta contiene un archivo, ¿cómo le ofrezco al cliente que lo descargue? He leído varios hilos similares aquí pero ninguno proporciona la respuesta que estoy buscando.

Por favor, por favor, no publique respuestas sugiriendo que no debería usar ajax para esto o que debería redirigir el navegador, porque nada de esto es una opción. Usar un formulario HTML simple tampoco es una opción. Lo que necesito es mostrar un cuadro de diálogo de descarga al cliente. ¿Se puede hacer esto y cómo?

Pavle Predic avatar Apr 18 '13 21:04 Pavle Predic
Aceptado

No te rindas tan rápido, porque esto se puede hacer (en los navegadores modernos) usando partes de 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));

O si usa 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
        }
    }
});
Jonathan Amend avatar May 22 '2014 03:05 Jonathan Amend

Cree un formulario, utilice el método POST, envíe el formulario; no es necesario un iframe. Cuando la página del servidor responda a la solicitud, escriba un encabezado de respuesta para el tipo mime del archivo y presentará un cuadro de diálogo de descarga. Lo he hecho varias veces.

Quiere un tipo de contenido de aplicación/descarga; simplemente busque cómo proporcionar una descarga para cualquier idioma que esté utilizando.

 avatar Apr 18 '2013 15:04

Me enfrenté al mismo problema y lo resolví con éxito. Mi caso de uso es este.

" Publique datos JSON en el servidor y reciba un archivo de Excel. Ese archivo de Excel lo crea el servidor y lo devuelve como respuesta al cliente. Descargue esa respuesta como un archivo con un nombre personalizado en el navegador "

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

El fragmento anterior solo hace lo siguiente

  • Publicar una matriz como JSON en el servidor mediante XMLHttpRequest.
  • Después de buscar contenido como un blob (binario), creamos una URL descargable y la adjuntamos a un enlace "a" invisible y luego hacemos clic en él.

Aquí debemos configurar cuidadosamente algunas cosas en el lado del servidor. Configuré algunos encabezados en Python Django HttpResponse. Debe configurarlos en consecuencia si utiliza otros lenguajes de programación.

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

Desde que descargué xls (excel) aquí, ajusté contentType al anterior. Debe configurarlo según su tipo de archivo. Puede utilizar esta técnica para descargar cualquier tipo de archivos.

Naren Yellavula avatar Jul 04 '2016 07:07 Naren Yellavula

¿Qué lenguaje del lado del servidor estás usando? En mi aplicación puedo descargar fácilmente un archivo desde una llamada AJAX configurando los encabezados correctos en la respuesta de PHP:

Configuración de encabezados del lado del servidor

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;

De hecho, esto "redireccionará" el navegador a esta página de descarga, pero como ya dijo @ahren en su comentario, no saldrá de la página actual.

Se trata de configurar los encabezados correctos, así que estoy seguro de que encontrarás una solución adecuada para el lenguaje del lado del servidor que estás usando si no es PHP.

Manejo del lado del cliente de respuesta

Suponiendo que ya sabe cómo realizar una llamada AJAX, en el lado del cliente ejecuta una solicitud AJAX al servidor. Luego, el servidor genera un enlace desde donde se puede descargar este archivo, por ejemplo, la URL 'de reenvío' a la que desea señalar. Por ejemplo, el servidor responde con:

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

Al procesar la respuesta, inyectas un iframeen tu cuerpo y estableces el iframeSRC en la URL que acabas de recibir de esta manera (usando jQuery para facilitar este ejemplo):

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

Si ha configurado los encabezados correctos como se muestra arriba, el iframe forzará un cuadro de diálogo de descarga sin tener que navegar el navegador fuera de la página actual.

Nota

Adición adicional en relación con su pregunta; Creo que es mejor devolver siempre JSON al solicitar cosas con tecnología AJAX. Una vez que haya recibido la respuesta JSON, podrá decidir en el lado del cliente qué hacer con ella. Tal vez, por ejemplo, más adelante desee que el usuario haga clic en un enlace de descarga a la URL en lugar de forzar la descarga directamente; en su configuración actual tendría que actualizar tanto el lado del cliente como el del servidor para hacerlo.

Robin van Baalen avatar Apr 18 '2013 15:04 Robin van Baalen