Descargar archivos con JavaScript y FileSaver

En los siguientes párrafos conocerás de qué forma puedes añadir a tu página web la funcionalidad de descargar archivos generados en el navegador, con JavaScript y la librería FileSaver.

FileSaver An HTML5 saveAs() FileSaver implementation

En éste artículo, he preparado un ejercicio práctico que hace uso de la librería. Haz click en la siguiente imagen para ver el resultado final en una ventana nueva:

Descargar archivos con JavaScript y FileSaver
Haz click sobre la imagen para ver el ejercicio acabado de éste tutorial

Si tienes poco tiempo y quieres pasar directamente a la práctica. Haz click aquí para salta directamente al tutorial

Explota todo el potencial de tu navegador

Exportar y guardar archivos directamente desde el navegador, puede ser una funcionalidad clave en muchos programas.

Especialmente si hablamos de softwares de gestión, dónde el usuario maneja datos, que posteriormente querrá guardar en su dispositivo.

Por poner algunos ejemplos, podría ser interesante extraer archivos en formatos como documentos de texto, imágenes o PDF.

De hecho, respecto a ésta última extensión, ya vimos una librería que resolvía el problema de generar y exportar documentos de ese tipo.

Genera archivos PDF con jsPDF

Pero ¿qué pasa si, como desarrolladores, necesitamos una opción más versátil?. 

Por ejemplo, podría darse una situación donde sea necesario extraer una imágen creada por el usuario, en ese caso, ya no nos serviría el recurso anterior.

Hoy descubriremos una herramienta capaz de descargar archivos de cualquier tipo, independientemente de su formato y extensión.

Estoy hablando de FileSaver.js. Una microlibrería JavaScript con una sencilla API para exportar documentos generados dinámicamente en el cliente, y guardarlos en local.

Ésta popular herramienta, creada por Eli Grey, tiene más de 19.1K estrellas en su repositorio de Github. Y en el momento de redactar estas líneas, acumula una media de 2.6M descargas semanales en Npm.

Puedes incorporar la librería en tu proyecto mediante el siguiente comando.

npm install file-saver

Utilizar FileSaver es extremadamente sencillo, de hecho, ésta librería consiste en una única función llamada “saveAs”.

import { saveAs } from 'file-saver';

La complejidad en sí, radica más en entender los argumentos con los que trabaja ésta función, así como en saber cómo construirlos. No te preocupes, en seguida entramos a verlo en detalle.

El método “saveAs” admite dos parámetros. El primero se trata de un objeto Blob, y el segundo de una cadena de texto, que representará el nombre del fichero a descargar.

Para construir un objeto Blob, es necesario apoyarse en la API de archivos que se encuentra en los navegadores modernos.

Un objeto Blob es la representación de un archivo de datos que puede ser leído como texto plano o como información binaria.

Es posible que ésta definición no te diga mucho, de modo que a continuación tienes un enlace a la descripción de ésta API de navegador.

https://developer.mozilla.org/es/docs/Web/API/Blob

Como verás, la implementación de ésta interfaz también es muy sencilla. El siguiente código de ejemplo, muestra cómo crear una instancia Blob con información de texto codificada como HTML.

const blob = new Blob(["<a id="a"><b id="b">hey!</b></a>"], {type: "text/html"});
FileSaver.saveAs(blob, "hello-world.html");

Tal y como se aprecia en el ejemplo, el primer parámetro del constructor son los datos que guardará la instancia. El segundo parámetro es el tipo MIME de dichos datos.

Todo fichero puede ser expresado como texto, o como un binario. Entender la diferencia entre estos dos tipos de codificaciones puede resultar útil para trabajar con la librería.

En el caso de los archivos de texto (como el HTML del ejemplo), los datos se representan cómo cadenas de texto. Algunos ejemplos de estos serían documentos .html .json o .txt.

Por otra parte, los archivos binarios contienen datos expresados como una serie de bits. De modo que otro programa pueda interpretar y leer su contenido. Ejemplos de este tipo de ficheros serían imágenes PNG y JPG, o ejecutables de un sistema operativo.

A pesar de que la herramienta que analizamos hoy nos permite crear ambos tipos, el ejercicio que implementaremos se centrará en la creación y guardado de un mapa de bits.

En concreto, te enseñaré a desarrollar una versión simplificada del famoso Microsoft Paint. 

Crea una app para pintar y descargar las imágenes en tu ordenador

Efectivamente, en las próximas líneas aprenderás a crear un programa donde el usuario podrá pintar y descargar la imágen resultante en su ordenador, con el nombre que quiera.

Empezaremos instalando la librería a través del comando que ya vimos. 

npm install file-saver

Para que puedas seguir cómodamente éste tutorial, te recomiendo que abras en una ventana a parte el código completo. Lo podrás encontrar en el enlace al repositorio que te dejo a continuación:

https://github.com/Danivalldo/libreriasjs/tree/master/FileSaverJs

Comenzaremos preparando la estructura HTML. Ésta se compone, inicialmente, por un contenedor con la clase “logo-container”. Como te puedes imaginar, servirá para mostrar un logo en la parte superior.

<div class="logo-container">
  <img src="icon.svg" alt="" />
</div>

Seguidamente incluímos una etiqueta “div” acompañada de la clase “painter-container”. Más adelante aprovecharemos este contenedor para anidar un elemento “canvas”.

De momento, solo ampliaremos la interfaz un poco más, insertando una serie de “inputs”. El primero será de tipo color, el segundo de tipo rango, y el tercero de tipo texto, acompañado de un botón de guardar.

<div class="painter-container">
  <div class="painter-actions">
    <div>
      <input type="color" value="#e46360" />
      <input type="range" min="1" max="100" value="10" />
      <div class="save-file-container">
        <input
          type="text"
          class="file-name-field"
          placeholder="Ponle un nombre a tu obra"
        /><button>Guardar</button>
      </div>
    </div>
  </div>
</div>

A continuación pasamos a definir los estilos de la interfaz que hemos creado. Lo haremos mediante SCSS, empezando por declarar con qué fuente mostramos los textos.

@import url("https://fonts.googleapis.com/css2?family=Permanent+Marker&display=swap");

En mi caso utilizare “Permanent Maker” de Google Fonts.

El siguiente paso será incluir los estilos generales para el “body” y el “html”.

body,
html {
  height: 100%;
}
body {
  margin: 0;
  padding: 0;
  font-family: "Permanent Marker", cursive;
}

Posicionamos el contenedor del logo de forma fija en la parte superior, dándole un tamaño máximo y un color de fondo.

.logo-container {
  background-color: #e46360;
  padding: 20px;
  max-width: 50px;
  position: fixed;
  top: 0;
  left: 50%;
  margin-left: -50px;
  z-index: 1;
  img {
    display: block;
    width: 100%;
  }
}

Cerramos el archivo index.scss dándo estilos al contenedor “painter-container”, así como a sus hijos.

.painter-container {
  height: 100%;
  position: relative;
  canvas {
    display: block;
    touch-action: none;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    box-sizing: border-box;
    cursor: crosshair;
  }
  .painter-actions {
    background-color: rgb(240, 240, 240);
    position: absolute;
    bottom: 0;
    left: 0;
    z-index: 1;
    width: 100%;
    & > div {
      max-width: 500px;
      margin: 0 auto;
      padding: 10px;
    }
    input {
      display: block;
      height: 35px;
      width: 100%;
      box-sizing: border-box;
      font-family: "Permanent Marker", cursive;
    }
    button {
      background-color: #e46360;
      color: #fff;
      border: none;
      font-family: "Permanent Marker", cursive;
      padding: 3px 40px;
      font-size: 1.3rem;
      border-radius: 4px;
    }
    .save-file-container {
      display: flex;
    }
  }
}

A partir de este punto, toca preparar la lógica JavaScript del programa

A decir verdad, nos llevará más tiempo programar la función de pintado, que implementar la librería FileSaver. Siéntete libre de saltar directamente al final, si solo te interesa ver cómo se implementa la función “saveAs”.

Iniciamos ésta fase creando un archivo JS al que llamaremos Painter. Lo guardaremos en el directorio “scripts” y allí definimos una clase con el mismo nombre. Posteriormente la iremos ampliando con métodos y propiedades.

class Painter {
};
export default Painter;

El constructor de nuestra clase admitirá dos argumentos, el contenedor dónde cargaremos un elemento “canvas”, y las propiedades del pincel. También allí preparamos las siguientes propiedades.

  • container: Una referencia al DOM del contenedor pasado como parámetros.
  • isPointerDown: Una variable booleana para determinar si el usuario está interactuando con el lienzo, o no.
  • prevPos: Un objeto para guardar la última posición del puntero en la pantalla.
  • brushProps: Las propiedades del pincel. Sólo guardaremos el tamaño y el color.
  • canvas: Una etiqueta canvas que crearemos en el momento de instanciar la clase.
  • ctx: Una referencia al contexto del canvas creado. Usaremos un contexto tipo “2d”.

A continuación te dejo el código del constructor, para que puedas analizarlo detenidamente.

constructor(canvasContainer, brushProps = {}) {
  this.container = canvasContainer;
  this.isPointerDown = false;
  this.prevPos = {
    x: 0,
    y: 0,
  };
  this.brushProps = {
    lineWidth: brushProps.lineWidth || 10,
    color: brushProps.color || "#5B48D9",
  };
  this.canvas = document.createElement("canvas");
  this.container.appendChild(this.canvas);
  this.ctx = this.canvas.getContext("2d");
  this.resizeCanvas();
  this.addListeners();
}

Como ves, al final de ésta función llamamos a los métodos “resizeCanvas” y “addListeners”. De modo que vamos a declararlos justo debajo.

En “addListeners” vinculamos la escucha de los siguiente eventos a métodos propios de la clase.

  • pointerdown: Nombre del evento para detectar si el usuario ha tocado la pantalla o ha presionado el botón del ratón
  • pointerup: Evento que se lanza cuando el usuario suelta el botón del ratón o deja de tocar la pantalla.
  • pointermove: Éste evento se activa cada vez que el usuario mueve el puntero (dedo o cursor) dentro del canvas
  • resize: Se aplica a la ventana entera, y se ejecuta cada vez que hay un reescalado de la ventana.

Y así es cómo queda el método.

addListeners() {
  this.canvas.addEventListener("pointerdown", this.onPointerDown.bind(this));
  this.canvas.addEventListener("pointerup", this.onPointerUp.bind(this));
  this.canvas.addEventListener("pointermove", this.onPointerMove.bind(this));
  window.addEventListener("resize", () => {
    this.resizeCanvas();
  });
}

En seguida veremos las funciones vinculadas al control del puntero, però primero vamos analizar el código de otros métodos como “resizeCanvas”.

En las tres primeras líneas de éste método actualizamos el tamaño del canvas a partir del ancho y alto de su contenedor. Tras hacerlo repintamos de blanco todo el lienzo. La última instrucción antes de cerrar la función será llamar a “updateBrush”.

resizeCanvas() {
  this.containerSize = this.container.getBoundingClientRect();
  this.canvas.width = this.containerSize.width;
  this.canvas.height = this.containerSize.height;
  this.ctx.fillStyle = "#FFF";
  this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
  this.updateBrush();
}

En “updateBrush” nos aseguraremos de actualizar las propiedades del pincel. Por consiguiente, éste método admite dos argumentos, el color y el tamaño nuevos. Para que el trazo del pincel sea más orgánico, definiremos las propiedades “lineCap” y “lineJoin”. Finalmente, actualizamos el color y el tamaño según los recibidos, y los actualizamos el objeto “brushProps”.

updateBrush(color, lineWidth) {
  this.ctx.lineCap = "round";
  this.ctx.lineJoin = "round";
  this.ctx.strokeStyle = color || this.brushProps.color;
  this.ctx.lineWidth = lineWidth || this.brushProps.lineWidth;
  this.brushProps = {
    color: this.ctx.strokeStyle,
    lineWidth: this.ctx.lineWidth,
  };
}

Acto seguido declaramos un método adicional para obtener la referencia al canvas.

getCanvas() {
  return this.canvas;
}

Concluímos nuestra clase añadiendo los métodos asociados a los eventos del puntero descritos anteriormente.

En “onPointerDown” actualizamos la variable “isPointerDown” a true, iniciamos un trazo nuevo utilizando el contexto del canvas, y guardamos la posición relativa del puntero en “prevPos”.

onPointerDown(e) {
  this.isPointerDown = true;
  this.ctx.beginPath();
  this.prevPos.x = e.clientX - this.containerSize.x;
  this.prevPos.y = e.clientY - this.containerSize.y;
}

El siguiente método es “onPointerMove”. Éste sólo se ejecutará si la variable   “isPointerDown” ha sido seteada. En caso de que así sea, seguirá el trazo iniciado tomando como inicio la posición de “prevPos” y como final la del puntero. Por último, actualizamos la nueva posición en “prevPos”.

onPointerMove(e) {
  e.preventDefault();
  if (!this.isPointerDown) {
    return;
  }
  this.ctx.moveTo(this.prevPos.x, this.prevPos.y);
  this.ctx.lineTo(e.clientX, e.clientY - this.containerSize.y);
  this.ctx.stroke();
  this.prevPos.x = e.clientX - this.containerSize.x;
  this.prevPos.y = e.clientY - this.containerSize.y;
}

Terminamos el servicio “Painter” con el método “onPointerUp”. Como verás, éste sencillamente se encarga de actualizar la variable “isPonterDown” para setearla como falsa.

onPointerUp(e) {
  this.isPointerDown = false;
}

Ya tan solo queda crear un objeto nuevo a partir de la clase que acabamos de declarar, y hacer uso de la librería para guardar la imagen creada.

Para ello, abriremos el archivo index.js en el editor de texto, e importamos los estilos, la clase Painter y la función “saveAs” de la librería FileSaver.

import "./SCSS/index.scss";
import Painter from "./scripts/Painter";
import { saveAs } from "file-saver";

Toda la lógica que vayamos a programar a partir de ahora se debe ejecutar sólo cuando la ventana esté totalmente lista. Por eso iniciaremos el script, escuchando el evento “load” en la variable global “window”.

window.addEventListener("load", () => {});

Dentro de la función de respuesta, guardamos en variables referencias a los elementos del DOM con los que trabajaremos.

const slider = document.querySelector('input[type="range"]');
const colorPicker = document.querySelector('input[type="color"]');
const nameField = document.querySelector(".file-name-field");
const saveBtn = document.querySelector("button");

Instanciamos un objeto “painter” a partir del servicio que creamos. 

Pasaremos una referencia al contenedor como primer argumento, para que la clase sepa dónde debe añadir la etiqueta canvas. Como segundo parámetro, enviamos un objeto JSON con el tamaño del pincel y el color, obtenidos de los inputs recogidos anteriormente.

const painter = new Painter(document.querySelector(".painter-container"), {
  lineWidth: slider.value,
  color: colorPicker.value,
});

Dichos inputs pueden ser modificados por el usuario. De modo que capturamos los cambios mediante el método “addEventListener”, y aprovechamos la función de respuesta, para actualizar el pincel a través del método “updateBrush”.

slider.addEventListener("change", (e) => {
  painter.updateBrush(null, e.target.value);
});
colorPicker.addEventListener("change", (e) => {
  painter.updateBrush(e.target.value);
});

Por último, pero no por ello menos importante, capturamos el evento de click sobre el botón del layout. Si el usuario no ha rellenado el campo para el nombre del archivo, se lo notificamos con una alerta.

En caso contrario, obtenemos el Blob del contenido del “canvas” mediante la función “toBlob”. En la función de “callback” llamamos a “saveAs” pasando ese objeto, y la cadena de texto para el nombre del archivo. La última línea se encargará de resetear el campo del nombre.

saveBtn.addEventListener("click", () => {
  const fileName = nameField.value;
  if (!fileName) {
    alert("Necesitas darle un nombre a la imagen");
    return;
  }
  painter.getCanvas().toBlob((blob) => {
    saveAs(blob, `${fileName}.png`);
    nameField.setAttribute("value", "");
  });
});

Mas recursos para descargar archivos con JavaScript y FileSaver

Tal como te dije, ha llevado más tiempo preparar la lógica de la interacción que ver en acción la librería, la cual solo ha ocupado una línea de código en todo nuestro programa.

Sin embargo, a mi entender, eso es una característica positiva de la librería. Como en muchas ocasiones se suele decir, menos es más.

Por cierto, en otro artículo de este blog, aplicamos lo aprendido hoy, para crear descargar una captura de pantalla generada con JavaScript. Te animo a que le heches un vistazo.

Crear capturas de pantalla con JavaScript

Para terminar, en el listado de a continuación te dejaré algún recurso adicional por si quieres seguir aprendiendo sobre lo visto hoy.

Un abrazo desarrolladores!

Deja un comentario