Seguro muchos han estado en esta situación: programaste una página web que solicita al usuario cierta información (datos personales, tomarse una foto, subir una imagen…) y tienes en pantalla todos los datos necesarios para generar un archivo de salida (un archivo xml, txt, png, docx…) pero tienes que enviar tooodos los datos al servidor únicamente para que al retornar salga el cuadro de dialogo para descargar el archivo.
Es una situación «vergonzosa» desperdiciar todo ese tiempo haciendo un post solo para mostrar el dialogo de descarga. Bueno, estos últimos meses/años los desarrolladores de navegadores web han hecho un gran esfuerzo por implementar varios aspectos del API de HTML5, y finalmente tenemos la posibilidad de crear y descargar un archivo directamente desde el navegador, usando solo Javascript. Bueno, no cualquier clase archivo, pero si los formatos de archivos cuya estructura sea conocida (txt, xml, doxc, pdf…) o que podamos generarla desde el API de HTML5 (png, jpg…).
Este artículo está dividido en dos secciones. En la primera parte crearemos una función que se encargará de descargar mediante Javascript un archivo (previamente generado) mostrando el típico dialogo de descarga del navegador, o descargarlo automáticamente, si el usuario así lo ha configurado. En la segunda parte del artículo veremos como generar archivos de texto simples desde Javascript para ser descargados desde el navegador.
Directo al punto: descargar un archivo desde Javascript
El siguiente bloque de código define la función que hará el trabajo. Veamos:
function descargarArchivo(contenidoEnBlob, nombreArchivo) {
//creamos un FileReader para leer el Blob
var reader = new FileReader();
//Definimos la función que manejará el archivo
//una vez haya terminado de leerlo
reader.onload = function (event) {
//Usaremos un link para iniciar la descarga
var save = document.createElement('a');
save.href = event.target.result;
save.target = '_blank';
//Truco: así le damos el nombre al archivo
save.download = nombreArchivo || 'archivo.dat';
var clicEvent = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': true
});
//Simulamos un clic del usuario
//no es necesario agregar el link al DOM.
save.dispatchEvent(clicEvent);
//Y liberamos recursos...
(window.URL || window.webkitURL).revokeObjectURL(save.href);
};
//Leemos el blob y esperamos a que dispare el evento "load"
reader.readAsDataURL(contenidoEnBlob);
};
Los comentarios en el código anterior son auto-explicativos, así que solo resaltaré los puntos clave: la función descargarArchivo()
recibe como primer parámetro un objeto Blob
(que según MDN es la forma preferida de leer un archivo usando un FileReader), el cual contiene el contenido que se desea que el usuario descargue. El segundo parámetro es el nombre que tendrá el archivo cuando inicie la descarga. Otro detalle es que el objeto window.URL
hasta hace poco requería el prefijo webkit
en Chrome, Safari y varios otros navegadores, así que por compatibilidad usamos un OR
(o ||
) para llamar a la función revokeObjectURL
con el objeto correcto al final.
Solo he probado la función en Google Chrome v29 y Mozilla Firefox v23, pero las características HTML5 que utilicé están soportadas por los navegadores más recientes, incluyendo Android 4 y (para mi sorpresa) IE 10.
La función anterior se ve interesante, pero no servirá de nada hasta que le pasemos un objeto Blob con el documento que queremos descargar.
Creando un nuevo archivo de texto
Para crear un archivo usando solo Javascript primero debemos recopilar la información que guardaremos en el archivo, y luego armaremos el contenido según la estructura que necesitemos. Pero claro, este proceso dependerá del formato de archivo. Veamos como sería el proceso para generar un archivo de texto:
//Función de ayuda: reúne los datos a exportar en un
//solo objeto
function obtenerDatos() {
return {
nombre: document.getElementById('textNombre').value,
telefono: document.getElementById('textTelefono').value,
fecha: (new Date()).toLocaleDateString()
};
};
//Genera un objeto Blob con los datos en un archivo TXT
function generarTexto(datos) {
var texto = [];
texto.push('Datos Personales:\n');
texto.push('Nombre: ');
texto.push(datos.nombre);
texto.push('\n');
texto.push('Teléfono: ');
texto.push(datos.telefono);
texto.push('\n');
texto.push('Fecha: ');
texto.push(datos.fecha);
texto.push('\n');
//El constructor de Blob requiere un Array en el primer
//parámetro así que no es necesario usar toString. El
//segundo parámetro es el tipo MIME del archivo
return new Blob(texto, {
type: 'text/plain'
});
};
document.getElementById('boton-txt').addEventListener('click', function () {
var datos = obtenerDatos();
descargarArchivo(generarTexto(datos), 'archivo.txt');
}, false);
La primera función, obtenerDatos()
, es solo una ayuda para recopilar la información necesaria para generar el archivo. En el ejemplo ésta obtiene el nombre y teléfono del usuario, almacenados en dos cajas de texto en pantalla, y agrega la fecha actual (en formato de cadena). Si los datos vienen de una tabla, de un canvas, o los pedirás mediante AJAX al servidor ésta será la función encargada de recopilar la información en un solo lugar, así el objeto devuelto por esta función lo puedes pasar como parámetro a cualquiera de las funciones que generen los diferentes formatos de salida que necesites.
La segunda función, generarTexto()
, se encargará de componer el contenido del archivo de texto a generar, usando los datos pasados como parámetro. Para concatenar las cadenas usaremos un Array, el cual será justamente el primer parámetro del constructor de nuestro Blob. Al crear el Blob es importante asignarle el tipo MIME correcto, ya que éste será usado por el navegador para decidir que debe hacer con el archivo (mostrarlo, abrirlo, descargarlo…) según las preferencias del usuario.
Al final del bloque de código podemos ver un ejemplo de cómo iniciar la descarga cuando el usuario haga clic en un botón en la página.
Creando un nuevo archivo XML
Ya vimos como crear un archivo de texto e iniciar la descarga mediante Javascript. ¿Qué tal si ahora generamos un archivo más estructurado y cambiamos el tipo MIME?
Vamos a agregar al código anterior un par de funciones adicionales para probar.
//Función de ayuda: "escapa" las entidades XML necesarias
//para los valores (y atributos) del archivo XML
function escaparXML(cadena) {
if (typeof cadena !== 'string') {
return '';
};
cadena = cadena.replace('&', '&')
.replace('<', '<')
.replace('>', '>')
.replace('"', '"');
return cadena;
};
//Genera un objeto Blob con los datos en un archivo XML
function generarXml(datos) {
var texto = [];
texto.push('<?xml version="1.0" encoding="UTF-8" ?>\n');
texto.push('<datos>\n');
texto.push('\t<nombre>');
texto.push(escaparXML(datos.nombre));
texto.push('</nombre>\n');
texto.push('\t<telefono>');
texto.push(escaparXML(datos.telefono));
texto.push('</telefono>\n');
texto.push('\t<fecha>');
texto.push(escaparXML(datos.fecha));
texto.push('</fecha>\n');
texto.push('</datos>');
//No olvidemos especificar el tipo MIME correcto :)
return new Blob(texto, {
type: 'application/xml'
});
};
document.getElementById('boton-xml').addEventListener('click', function () {
var datos = obtenerDatos();
descargarArchivo(generarXml(datos), 'archivo.xml');
}, false);
El bloque de código anterior nos permitirá crear un archivo XML válido para que el usuario lo descargue. Primero, la función escaparXML()
la usaremos para preparar el contenido del archivo, ya que hay caracteres que debemos escapar para que el archivo generado sea válido.
Al igual que hicimos con el archivo de texto en el bloque de código anterior, la función generarXml
creará el contenido XML del archivo, que no es más que un archivo de texto con estructura. Al final de la función generamos el Blob con el tipo MIME correspondiente para un archivo XML.
Si lo prefieres, puedes generar el contenido del archivo XML mediante algún mecanismo diferente. Por ejemplo usando document.implementation.createDocument
para generar un documento XML (la función es soportada por lo navegadores actuales, incluyendo IE9), del cual luego puedes convertir en una cadena mediante XMLSerializer.serializeToString()
.
Para terminar vemos un ejemplo de como generar y descargar el archivo cuando el usuario hace clic en un botón en pantalla.
Si quieres probar el código del artículo solo tienes que insertar el mismo en la siguiente plantilla HTML (o probar directamente este jsFiddle)
<!DOCTYPE
HTML>
<html
lang="es">
<head>
<meta
charset="utf-8">
<title>Crear y descargar archivos con Javascript</title>
<script type="text/javascript">
function
descargarArchivo(contenidoEnBlob, nombreArchivo) {
//CÓDIGO AQUÍ
};
function
obtenerDatos() {
//CÓDIGO AQUÍ
};
function
escaparXML(cadena) {
//CÓDIGO AQUÍ
};
function
generarTexto(datos) {
//CÓDIGO AQUÍ
};
function
generarXml(datos) {
//CÓDIGO AQUÍ
};
window.addEventListener('DOMContentLoaded', function(){
document.getElementById('boton-xml').addEventListener('click', function
() {
var
datos = obtenerDatos();
descargarArchivo(generarXml(datos), 'archivo.xml');
}, false);
document.getElementById('boton-txt').addEventListener('click', function
() {
var
datos = obtenerDatos();
descargarArchivo(generarTexto(datos), 'archivo.txt');
}, false);
}, false);
</script>
</head>
<body>
<h3>Datos Personales:</h3>
<div>
<label
for="textNombre">Nombre:</label>
<input
type="text"
id="textNombre"
value="Pedro Pérez <Ing. & Lic.>" /><br/>
<label
for="textTelefono">Teléfono:</label>
<input
type="text"
id="textTelefono"
value="+58.212.555.5555"
/><br/>
</div><br/>
<input
type="button"
id="boton-txt"
value="Descargar TXT"
/><br/>
<input
type="button"
id="boton-xml"
value="Descargar XML"
/>
</body>
</html>
Spoiler Alert: para un próximo artículo veremos como pedir una imagen al usuario, aplicarle algunos filtros y luego permitirle descargarla, todo esto directamente desde el navegador por medio de Javascript.
Derecho de uso
Los contenidos generados por el autor de este artículo (explicaciones, código fuente, y archivos adjuntos creados por el autor) están disponibles bajo licencia CC BY-SA 3.0, y pueden ser usados, derivados y compartidos bajo los términos indicados en la misma. Los contenidos no generados por el autor de este artículo son propiedad de sus respectivos dueños y están regidos por las licencias que estos hayan dispuesto.
Cita del día
Getting information off the Internet is like taking a drink from a fire hydrant.
Mitchell D. Kapor
29 respuestas a “Crear y descargar archivos con Javascript”
Excelente Web . Continua este buen trabajo.Contiene un enfoque impresionante sobre el tema y los comentarios son realmente acertados.
Solo mencionar que estoy sorprendido por haber tropezado con esta web
Posiblemente tienes el mejor sitio
web sobre el tema.
Muchas gracias 🙂
¡Gracias Coy! 😀 me gusta escribir y darle un enfoque didáctico y práctico, aunque no tengo mucho tiempo libre para hacerlo. Justo ahora estoy preparando el próximo artículo, el tema de aplicar filtros en imágenes con Javascript llamó mucho mi atención pero debía escribir este primero 🙂
Posiblemente tienes el mejor sitio web sobre el tema.
Gracias 🙂
da gusto leer explicaciones tan claras y detalladas como las tuyas
Gracias! Me desespero cuando tengo que leer algo varias veces porque no está claro o está mal redactado 😛
Excelente la información y la forma en que la explica, tengo una duda, el archivo que se crea, en donde se almacena antes de descargarlo?
Saludos.
Saludos Malko! el archivo generado está siempre en memoria (en el navegador, sin «archivos temporales») hasta que el usuario lo guarde en disco: En el código de la sección «Creando un nuevo archivo de texto», la función
generarTexto()
tiene el contenido del archivo en la variabletexto
(un array temporal) y te devuelve un objeto Blob que contiene el archivo generado; en el ejemplo envío el Blob directamente a la función de descarga y lo almaceno temporalmente en forma de URL dentro del link creado (en la variablesave
) hasta que se simula el clic que inicia la descarga.Hola,
Estaba con probando tú codigo y me he encontrado con que en IE10 y 11 da problemas, en la linea que dice var clicEvent = new MouseEvent(‘click’, {… da un error de que el objeto no soporta la propiedad y no genera el archivo, en FF y chrome sin problema.
He mirado a ver cual podría ser una alternativa pero no la encuentro, sabrías tu decirme una?
Gracias!
Qué tal,
Creo que tengo el mismo problema que la persona del comentario de arriba, al parecer todo funciona bien pero la función de descarga no genera el archivo, sino que abre una pestaña nueva que contiene la información en el navegador. Estoy utilizando Mozilla en entorno Linux.
Te agradecería una solución al respecto, por lo demás, excelente artículo.
¡Saludos!
Hola!
Me parece una función tremenda. Ya era hora de darle un poco de «vida» a JS… se me ocurren varias utilidades para esta función… una pregunta ¿Se pueden leer ficheros locales con alguna función similar?
Me guardo tu link para hacer un par de experimentos, ya comentaré 😛
Saludos y gracias por compartir el conocimiento!
Hola! me parece genial el aporte!.
Lo eh utilizado y funciona perfectamente, el único problema que tengo es que si mi archivo presenta símbolos o caracteres especiales me toma cualquier cosa. Habría alguna solución para ello?
Hola Andrea! ¿te da error al generar el archivo TXT o XML? ¿y que programa usas para abrir el archivo una vez que lo descargas? en general el único problema que debería ocurrir es cuando tienes caracteres especiales en el XML, pero de eso se ocupa la función «escaparXML()» y el «UTF8» en la declaración (acabo de probar nuevamente el jsfiddle de ejemplo y funciona con acentos ampersands «&» y paréntesis angulares http://jsfiddle.net/roimergarcia/XLqsf/ ).
Para ver cual es el problema intenta abrir el archivo descargado con un editor «avanzado», como «XML Notepad» o «SublimeText».
El archivo que me interesa generar es el TXT. Los programas que utilizo para abrir el archivo es Gedit o SublimeText. Mi archivo contiene caracteres especiales y símbolos como < y me lo toma como <. El problema es que este archivo descargado debo ejecutarlo y se presentan errores de sintaxis por este tema. Existe alguna manera de solucionarlo?.
Muchas gracias por su respuesta.
OK, probé nuevamente el link con el ejemplo para generar un TXT que contiene «» y «&», y sale normalmente. Creo que lo que pasa es que estas usando la función “escaparXML()” al generar el TXT: en el TXT no es necesario, solo la utilizo en el xml para codificar los caracteres no válidos. Si no es este el problema puedes pasarme tu código en un jsFiddle (o pastebin, o lo que uses 🙂 para probarlo; estaré en línea como a las 20:00 VET (en unas 11 horas).
Las únicas funciones que utilizo en mi código es el generar texto y descargar archivo. No se cual seria el problema con los símbolos como < y &, me toman como si fuera un archivo XML descargado.
Genial, es de gran ayuda pero no consigo que en el archivo descargado me aparezcan los retornos de carro y los necesito sino es ilegible :/
La respuesta a mi problema era muy simple.
El problema se genera al manejar el objeto Blob el cual descompone tal y como debe hacerlo y no reconoce los caracteres de escape.
Pues bien mi solucion fue simple: String.fromCharCode() –> Siendo el codigo ASCII correspondiente «13» para retorno de carro. 🙂
Gracias por el aporte Juan! en mis pruebas usando ‘\n’ se formateaba correctamente, de todas formas en caso de que a alguien más le ocurra ya tiene solución 🙂
Para Internet Explorer 10+ agregué estas dos líneas de código y funciona perfecto
function descargarArchivo(contenidoEnBlob, nombreArchivo) {
if (navigator.msSaveBlob) { // IE10
return navigator.msSaveBlob(contenidoEnBlob, nombreArchivo);
}
Gracias por el aporte Ramón! 🙂 ya por costumbre no pruebo mucho en IE :S excepto tal vez la última versión que es «bastante cercana al estándar», lástima que mucha gente aun no suelta las versiones viejas del IE; conozco una empresa que tengo usa IE7, y no lo cambia debido a sus «políticas de actualización de software» :p
Muchas gracias por el post, funciona perfectamente y me sirvio de gran ayuda.
Ahora tengo un pequeño problema y es que en vez de un unico fichero XML tengo que generar varios con datos de una tabla html. Recorro la tabla con un bucle for, obtengo los datos y se generan todos perfectamente pero logicamente se abren todos en el navegador. Había pensado usar alguna api (jsZip) para almacenar en un ZIP y guadar todos pero no se como puedo obtener los ficheros..¿podrías ayudarme?
Gracias. Un saludo
Que tal amigo muy interesante el codigo, tengo un problema que realmente no doy con la solucion:
Utilice http://jsfiddle.net/roimergarcia/XLqsf/ de ahi copie el codigo y me da error en la linea 73 en caso de tener el script dentro del html. Lo probe con un js externo y me sigue pasando.
Saludos espero tu respuesta
Perdon por no poner el error: TypeError: document.getElementById(…) is null. Saludos
Hola Maxi! OK, tardé tanto en responder que supongo ya resolviste, 😛 de todas formas: por el mensaje de error parece que no encuentra uno de los dos inputs. Fijate que en tu HTML tengan el mismo ID que tiene en la linea que da error; si esto está correcto pon un «console.log(document.getElementById(‘textNombre’))» antes de la linea que da el error a ver si te sale en la consola el input correcto.
Genial, estoy desarrollando con la idea de aprovechar el almacenamiento local para una pequeña base de datos pero depende de cosas como el historial y los datos almacenados por el navegador, con esta idea puedo hacer que el usuario al cerrar la página «guarde» los datos almacenados. Muchas gracias
como puedo hacer para verificar que un archivo se termino de descargar?, porque quiero eliminarlo del servidor cuando se termine de descargar
Hola! bueno, según te entiendo te refieres a un archivo que está en el servidor (este artículo se refiere a archvos creados con javascript en el navegador). Hasta donde se, no es posible saberlo directamente, ya que el navegador no tiene un evento que indique cuando termina la descarga.
Si el archivo lo generas en el servidor, una posible solución es que lo crees siguiendo un patrón con el nombre, y crear un «limpiador» que elimine los archivos temporales que tengan más de X horas/días de antigüedad.
Ahora, si es un archivo «único, propiedad del usuario» y quieres que se elimine si y solo si logra descargarlo, hay una posible solución usando una cookie (requiere código tanto JavaScript como de servidor). Te dejo un ilnk a la segunda solución (en inglés): http://gruffcode.com/2010/10/28/detecting-the-file-download-dialog-in-the-browser/
Muchas gracias por la respuesta
Buenas noches.
Si en php genero el archivo texto, dentro de una carpeta del servidor, como podria adaptar este codigo, para que por medio de javascript pueda bajarlo al equipo de usuario?
Saludos cordiales.