Manejar descarga de archivos desde ajax post


Tengo una aplicación javascript que envía solicitudes POST ajax a una determinada URL. La respuesta puede ser una cadena JSON o un archivo (como un 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 ofrezco al cliente descargarlo? He leído una serie de hilos similares aquí, pero ninguno de ellos proporciona la respuesta que estoy buscando.

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

EDITAR:

Aparentemente, esto no se puede hacer, pero hay una solución simple, como sugiere la respuesta aceptada. Para cualquiera que se encuentre con este problema en el futuro, así es como lo resolví:

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

Así que básicamente, solo genera un HTML formulario con los mismos parámetros que se utilizaron en AJAX solicitud y enviarlo.

Author: jwfearn, 2013-04-18

14 answers

Crea un formulario, usa el método POST, envía el formulario - no hay necesidad de 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 diálogo de descarga - he hecho esto varias veces.

Si desea contenido-tipo de aplicación/descarga - simplemente busque cómo proporcionar una descarga para cualquier idioma que esté utilizando.

 96
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-18 15:00:21

No te rindas tan rápido, porque esto se puede hacer (en los navegadores modernos) usando partes del FileAPI:

Editar 2017-09-28: Actualizado para usar File constructor cuando esté disponible para que funcione en Safari > = 10.1.

Editar 2015-10-16: jQuery ajax no es capaz de manejar las respuestas binarias correctamente (no se puede establecer responseType), por lo que es mejor utilizar una llamada 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));

Aquí está la versión antigua usando jQuery.ajax. Podría destrozar los datos binarios cuando el la respuesta se convierte en una cadena de algún conjunto de caracteres.

$.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
        }
    }
});
 449
Author: Jonathan Amend,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-28 15:52:59

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

Configurar 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;

Esto de hecho 'redirigirá' el navegador a esta página de descarga, pero como @ahren ya dijo en su comentario, no se alejará de la página actual.

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

Manejando la respuesta del lado del cliente

Suponiendo que ya sabe cómo hacer una llamada AJAX, en el lado del cliente ejecuta una solicitud AJAX al servidor. A continuación, el servidor genera un enlace desde el que se puede descargar este archivo, por ejemplo, la URL 'reenviar' a la que desea apuntar. 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, inyecta un iframe en tu cuerpo y establece el SRC de iframe en la URL que acabas de recibir como esto (usando jQuery para facilitar este ejemplo):

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

Si ha establecido los encabezados correctos como se muestra arriba, el iframe forzará un diálogo de descarga sin navegar por 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 cuando se solicitan cosas con tecnología AJAX. Después de recibir la respuesta JSON, puede decidir qué hacer con ella en el lado del cliente. Tal vez, por ejemplo, más adelante si desea 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 cliente como el servidor para hacerlo.

 28
Author: Robin van Baalen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-09-23 19:39:36

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 excel. Ese archivo de Excel es creado por el servidor y devuelto como respuesta al cliente. Descargue esa respuesta como un archivo con 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 de código anterior solo está haciendo lo siguiente

  • Publicando un array como JSON en el servidor usando XMLHttpRequest.
  • Después de obtener el contenido como un blob (binario), están creando una URL descargable y adjuntándola al enlace invisible "a" y luego haciendo clic en él.

Aquí necesitamos establecer cuidadosamente algunas cosas en el lado del servidor. Establecí algunas cabeceras 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 descargo xls(excel) aquí, ajusté ContentType a uno superior. Debe configurarlo de acuerdo con su tipo de archivo. Puede utilizar esta técnica para descargar cualquier tipo de archivos.

 26
Author: Naren Yellavula,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-09 12:38:11

Para aquellos que buscan una solución desde una perspectiva angular, esto funcionó para mí:

$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();
});
 18
Author: Tim Hettler,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-07 19:05:21

Veo que ya has encontrado una solución, sin embargo, solo quería añadir algo de información que puede ayudar a alguien tratando de lograr lo mismo con grandes solicitudes de POST.

Tuve el mismo problema hace un par de semanas, de hecho, no es posible lograr una descarga "limpia" a través de AJAX, el Grupo de Filamentos creó un plugin jQuery que funciona exactamente como ya lo has descubierto, se llama jQuery File Download sin embargo, hay una desventaja de esta técnica.

Si estás enviando grandes solicitudes a través de AJAX (digamos archivos +1MB) que tendrá un impacto negativo en la capacidad de respuesta. En conexiones lentas a Internet tendrás que esperar mucho hasta que se envíe la solicitud y también esperar a que se descargue el archivo. No es como un instante "click" => "popup" => "download start". Es más como "click" = > "wait until data is sent" = > "wait for response" = > "download start" lo que hace que parezca que el archivo duplica su tamaño porque tendrás que esperar a que se envíe la solicitud a través de AJAX y recuperarlo como un archivo descargable.

Si está trabajando con archivos de tamaño pequeño

Mi aplicación permite a los usuarios exportar imágenes generadas dinámicamente, estas imágenes se envían a través de solicitudes POST en formato base64 al servidor (es la única forma posible), luego se procesan y se envían de vuelta a los usuarios en forma de .png, .los archivos jpg, las cadenas base64 para imágenes + 1MB son enormes, esto obliga a los usuarios a esperar más de lo necesario para que el archivo comience a descargarse. En conexiones lentas a Internet puede ser realmente molesto.

Mi solución para esto fue escribir temporalmente el archivo en el servidor, una vez que esté listo, generar dinámicamente un enlace al archivo en forma de un botón que cambia entre "Por favor espere..."y" Descargar " estados y, al mismo tiempo, imprimir la imagen base64 en una ventana emergente de vista previa para que los usuarios pueden "clic derecho" y guardarlo. Esto hace que todo el tiempo de espera más soportable para los usuarios, y también acelerar las cosas.

Actualizar Sep 30, 2014:

Han pasado meses desde que publiqué esto, finalmente he encontrado un mejor enfoque para acelerar las cosas cuando se trabaja con grandes cadenas base64. Ahora almaceno cadenas base64 en la base de datos (usando los campos longtext o longblog), luego paso su ID de registro a través de la descarga del archivo jQuery, finalmente en el archivo de script de descarga consultola base de datos usando este ID para extraer la cadena base64 y pasar a través de la función de descarga.

Descargar Ejemplo de script:

<?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;
?>

Sé que esto va mucho más allá de lo que preguntó el OP, sin embargo, sentí que sería bueno actualizar mi respuesta con mis hallazgos. Cuando estaba buscando soluciones a mi problema, leí muchos hilos"Descargar desde AJAX POST data" que no me dieron la respuesta que estaba buscando, espero que esta información ayude a alguien que busca lograr algo como esto.

 11
Author: José SAYAGO,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-30 22:00:52

Así es como conseguí que esto funcionara 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();
  }
});

Respuesta actualizada usando download.js

$.ajax({
  url: '<URL_TO_FILE>',
  success: download.bind(true, "<FILENAME_TO_SAVE_WITH_EXTENSION>", "<FILE_MIME_TYPE>")
});
 8
Author: Mayur Padshala,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-30 19:06:06

Quiero señalar algunas dificultades que surgen al usar la técnica en la respuesta aceptada, es decir, usar un formulario post:

  1. No puede establecer encabezados en la solicitud. Si su esquema de autenticación incluye encabezados, un Json-Web-Token pasado en el encabezado de autorización, tendrá que encontrar otra forma de enviarlo, por ejemplo, como un parámetro de consulta.

  2. Realmente no se puede decir cuando la solicitud ha terminado. Bueno, puede utilizar una cookie que se establece en la respuesta, como hecho por jquery.FileDownload, pero está lejos de ser perfecto. No funcionará para solicitudes concurrentes y se romperá si nunca llega una respuesta.

  3. Si el servidor responde con un error, el usuario será redirigido a la página de error.

  4. Solo puede usar los tipos de contenido soportados por un formulario . Lo que significa que no puedes usar JSON.

Terminé usando el método de guardar el archivo en S3 y enviar una URL pre-firmada para obtener el file.

 5
Author: tepez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-11 12:17:07

Como otros han dicho, puede crear y enviar un formulario para descargar a través de una solicitud POST. Sin embargo, no tienes que hacerlo manualmente.

Una biblioteca muy simple para hacer exactamente esto es jquery.redirect . Proporciona una API similar al método estándar jQuery.post:

$.redirect(url, [values, [method, [target]]])
 2
Author: KurtPreston,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-22 20:29:31

Esta es una pregunta de hace 3 años, pero hoy tuve el mismo problema. Miré su solución editada, pero creo que puede sacrificar el rendimiento porque tiene que hacer una doble solicitud. Así que si alguien necesita otra solución que no implique llamar al servicio dos veces, entonces esta es la forma en que lo hice:

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

Este formulario solo se usa para llamar al servicio y evitar usar una ventana.ubicación(). Después de eso, simplemente tiene que hacer un envío de formulario desde jquery para llamar a la servicio y obtener el archivo. Es bastante simple, pero de esta manera puedes hacer una descarga usando un POST. Ahora que esto podría ser más fácil si el servicio que está llamando es un GET , pero ese no es mi caso.

 2
Author: Jairo Miranda,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-06 23:58:14

Aquí está mi solución usando una forma oculta temporal.

//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();

Tenga en cuenta que yo uso masivamente jQuery, pero usted puede hacer lo mismo con JS nativo.

 2
Author: Ludovic Martin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-01 09:28:56

Usé este Protector de archivos.js . En mi caso con archivos csv, hice esto (en coffescript):

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

Creo que para el caso más complicado, los datos deben procesarse correctamente. Bajo el capó FileSaver.js implementar el mismo enfoque de la respuesta de Jonathan Amend .

 1
Author: Armando,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:26:36

Véase: http://www.henryalgus.com/reading-binary-files-using-jquery-ajax / devolverá un blob como respuesta, que luego se puede poner en filesaver

 1
Author: Samantha Adrichem,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-27 12:54:16

Para obtener Jonathan Enmienda respuesta para trabajar en el borde hice los siguientes cambios:

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

A esto

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

Preferiría haber publicado esto como un comentario, pero no tengo suficiente reputación para eso

 1
Author: fstrandner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-13 08:24:50