Cómo leer archivos en JavaScript a través de las API de archivos

HTML5 Rocks

Introducción

Por fin, HTML5 ofrece una forma estándar de interactuar con archivos locales a través de la especificación del API de archivos. El API de archivos se puede utilizar, por ejemplo, para crear una vista previa en miniatura de imágenes mientras se envían al servidor o para permitir que una aplicación guarde una referencia de un archivo mientras el usuario está sin conexión. También se podría utilizar lógica de cliente para verificar si el tipo MIME de un archivo subido coincide con la extensión del archivo o para restringir el tamaño de una subida.

A continuación se indican las interfaces que ofrece la especificación para acceder a archivos desde un sistema de archivos "local".

  1. File: representa un archivo individual que proporciona información de solo lectura (por ejemplo, el nombre, el tamaño del archivo, el tipo MIME y una referencia al control del archivo).
  2. FileList: representa una secuencia de conjunto de objetos File (tanto la secuencia <input type="file" multiple> como arrastrar un directorio de archivos desde el escritorio se consideran ejemplos de esta interfaz).
  3. Blob: permite fragmentar un archivo en intervalos de bytes.

Cuando se utiliza junto con las estructuras de datos anteriores, la interfaz de FileReader se puede utilizar para leer un archivo de forma asíncrona mediante el control de eventos de JavaScript. Por lo tanto, se puede controlar el progreso de una lectura, detectar si se han producido errores y determinar si ha finalizado una carga. El modelo de evento de XMLHttpRequest guarda muchas semejanzas con las API.

Nota: en el momento de redactar este tutorial, las API necesarias para trabajar con archivos locales son compatibles con Chrome 6.0 y Firefox 3.6. A partir de Firefox 3.6.3, no se admite el método File.slice().

Cómo seleccionar archivos

En primer lugar, se debe comprobar que el navegador sea totalmente compatible con el API de archivos:

// Check for the various File API support.
if (window.File && window.FileReader && window.FileList && window.Blob) {
  // Great success! All the File APIs are supported.
} else {
  alert('The File APIs are not fully supported in this browser.');
}

Si tu aplicación solo va a utilizar algunas de estas API, modifica este fragmento en consecuencia.

Uso de entradas de formulario para seleccionar archivos

La forma más sencilla de cargar un archivo es utilizar un elemento <input type="file"> estándar. JavaScript devuelve la lista de objetos File seleccionados como una secuencia FileList. A continuación, se muestra un ejemplo en el que se utiliza el atributo "multiple" para permitir la selección simultánea de varios archivos:

<input type="file" id="files" name="files[]" multiple />
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    var files = evt.target.files; // FileList object

    // files is a FileList of File objects. List some properties.
    var output = [];
    for (var i = 0, f; f = files[i]; i++) {
      output.push('<li><strong>', escape(f.name), '</strong> (', f.type || 'n/a', ') - ',
                  f.size, ' bytes, last modified: ',
                  f.lastModifiedDate.toLocaleDateString(), '</li>');
    }
    document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

Ejemplo: uso de entradas de formulario para seleccionar archivos. ¡Haz una prueba!

Uso de la acción de arrastrar y soltar para seleccionar archivos

Otra técnica de carga de archivos consiste en arrastrar archivos nativos desde el escritorio y soltarlos en el navegador. Podemos modificar ligeramente el ejemplo anterior para incluir esta técnica.

<div id="drop_zone">Drop files here</div>
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    evt.stopPropagation();
    evt.preventDefault();

    var files = evt.dataTransfer.files; // FileList object.

    // files is a FileList of File objects. List some properties.
    var output = [];
    for (var i = 0, f; f = files[i]; i++) {
      output.push('<li><strong>', escape(f.name), '</strong> (', f.type || 'n/a', ') - ',
                  f.size, ' bytes, last modified: ',
                  f.lastModifiedDate.toLocaleDateString(), '</li>');
    }
    document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
  }

  function handleDragOver(evt) {
    evt.stopPropagation();
    evt.preventDefault();
    evt.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
  }

  // Setup the dnd listeners.
  var dropZone = document.getElementById('drop_zone');
  dropZone.addEventListener('dragover', handleDragOver, false);
  dropZone.addEventListener('drop', handleFileSelect, false);
</script>

Ejemplo: uso de la acción de arrastrar y soltar para seleccionar archivos. ¡Haz una prueba!

Suelta los archivos aquí.

Nota: algunos navegadores tratan los elementos <input type="file"> como destinos donde soltar archivos nativos. Intenta arrastrar los archivos al campo de introducción de contenido del ejemplo anterior.

Cómo leer archivos

Ahora viene la parte divertida.

Después de obtener una referencia de File, crea instancias de un objeto FileReader para leer su contenido en memoria. Cuando finaliza la carga, se activa el evento onload del lector y se puede utilizar su atributo result para acceder a los datos del archivo.

A continuación se indican las cuatro opciones de lectura asíncrona de archivo que incluye FileReader.

  • FileReader.readAsBinaryString(Blob|File): la propiedad result contendrá los datos del archivo/objeto BLOB en forma de cadena binaria. Cada byte se representa con un número entero comprendido entre 0 y 0,255, ambos incluidos.
  • FileReader.readAsText(Blob|File, opt_encoding): la propiedad result contendrá los datos del archivo/objeto BLOB en forma de cadena de texto. De forma predeterminada, la cadena se decodifica con el formato "UTF-8". Utiliza el parámetro de codificación opcional para especificar un formato diferente.
  • FileReader.readAsDataURL(Blob|File): la propiedad result contendrá los datos del archivo/objeto BLOB codificados como una URL de datos.
  • FileReader.readAsArrayBuffer(Blob|File): la propiedad result contendrá los datos del archivo/objeto BLOB como un objeto ArrayBuffer.

Una vez que se ha activado uno de estos métodos de lectura en el objeto FileReader, se pueden utilizar los eventos onloadstart, onprogress, onload, onabort, onerror y onloadend para realizar un seguimiento de su progreso.

En el ejemplo que se muestra a continuación, se excluyen las imágenes de los elementos seleccionados por el usuario, se activa reader.readAsDataURL() en el archivo y se muestra una miniatura estableciendo una URL de datos como valor del atributo "src".

<style>
  .thumb {
    height: 75px;
    border: 1px solid #000;
    margin: 10px 5px 0 0;
  }
</style>

<input type="file" id="files" name="files[]" multiple />
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    var files = evt.target.files; // FileList object

    // Loop through the FileList and render image files as thumbnails.
    for (var i = 0, f; f = files[i]; i++) {

      // Only process image files.
      if (!f.type.match('image.*')) {
        continue;
      }

      var reader = new FileReader();

      // Closure to capture the file information.
      reader.onload = (function(theFile) {
        return function(e) {
          // Render thumbnail.
          var span = document.createElement('span');
          span.innerHTML = ['<img class="thumb" src="', e.target.result,
                            '" title="', escape(theFile.name), '"/>'].join('');
          document.getElementById('list').insertBefore(span, null);
        };
      })(f);

      // Read in the image file as a data URL.
      reader.readAsDataURL(f);
    }
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

Ejemplo: lectura de archivos. ¡Haz una prueba!

Intenta realizar este ejemplo con un directorio de imágenes.


Fragmentación de archivos

En algunos casos, leer el archivo completo en la memoria no es la mejor opción. Supongamos, por ejemplo, que quieres crear una herramienta de subida de archivos asíncrona. Para acelerar la subida, se podría leer y enviar el archivo en diferentes fragmentos de intervalos de bytes. El componente del servidor se encargaría de reconstruir el contenido del archivo en el orden correcto.

Afortunadamente, la interfaz File es compatible con un método de fragmentación. El método utiliza un byte de inicio como primer argumento, un byte de finalización como segundo argumento y una cadena de introducción de contenido de opción como tercer argumento. La semántica de este método ha cambiado recientemente, así que en este fragmento se incluyen prefijos del proveedor:

if (file.webkitSlice) {
  var blob = file.webkitSlice(startingByte, endindByte);
} else if (file.mozSlice) {
  var blob = file.mozSlice(startingByte, endindByte);
}
reader.readAsBinaryString(blob);

En el ejemplo que aparece a continuación se muestran fragmentos de lectura de un archivo. Ten en cuenta que este método utiliza el evento onloadend y comprueba evt.target.readyState, en lugar de utilizar el evento onload.

<style>
  #byte_content {
    margin: 5px 0;
    max-height: 100px;
    overflow-y: auto;
    overflow-x: hidden;
  }
  #byte_range { margin-top: 5px; }
</style>

<input type="file" id="files" name="file" /> Read bytes: 
<span class="readBytesButtons">
  <button data-startbyte="0" data-endbyte="4">1-5</button>
  <button data-startbyte="5" data-endbyte="14">6-15</button>
  <button data-startbyte="6" data-endbyte="7">7-8</button>
  <button>entire file</button>
</span>
<div id="byte_range"></div>
<div id="byte_content"></div>

<script>
  function readBlob(opt_startByte, opt_stopByte) {

    var files = document.getElementById('files').files;
    if (!files.length) {
      alert('Please select a file!');
      return;
    }

    var file = files[0];
    var start = parseInt(opt_startByte) || 0;
    var stop = parseInt(opt_stopByte) || file.size - 1;

    var reader = new FileReader();

    // If we use onloadend, we need to check the readyState.
    reader.onloadend = function(evt) {
      if (evt.target.readyState == FileReader.DONE) { // DONE == 2
        document.getElementById('byte_content').textContent = evt.target.result;
        document.getElementById('byte_range').textContent = 
            ['Read bytes: ', start + 1, ' - ', stop + 1,
             ' of ', file.size, ' byte file'].join('');
      }
    };

    if (file.webkitSlice) {
      var blob = file.webkitSlice(start, stop + 1);
    } else if (file.mozSlice) {
      var blob = file.mozSlice(start, stop + 1);
    }
    reader.readAsBinaryString(blob);
  }
  
  document.querySelector('.readBytesButtons').addEventListener('click', function(evt) {
    if (evt.target.tagName.toLowerCase() == 'button') {
      var startByte = evt.target.getAttribute('data-startbyte');
      var endByte = evt.target.getAttribute('data-endbyte');
      readBlob(startByte, endByte);
    }
  }, false);
</script>

Ejemplo: fragmentación de archivo. ¡Haz una prueba!

Bytes de lectura:

Control del progreso de una lectura

Una de las funciones que se pueden disfrutar gratuitamente al utilizar el control de eventos de tipo asíncrono es la de control del progreso de la lectura de un archivo. Esto resulta útil para leer archivos de gran tamaño, detectar errores y saber cuándo se ha completado una lectura.

Los eventos onloadstart y onprogress se pueden utilizar para controlar el progreso de una lectura.

En el ejemplo que aparece a continuación, se muestra una barra de progreso que permite controlar el estado de la lectura. Para ver cómo funciona el indicador de progreso, intenta utilizar un archivo grande o un archivo de una unidad remota.

<style>
  #progress_bar {
    margin: 10px 0;
    padding: 3px;
    border: 1px solid #000;
    font-size: 14px;
    clear: both;
    opacity: 0;
    -moz-transition: opacity 1s linear;
    -o-transition: opacity 1s linear;
    -webkit-transition: opacity 1s linear;
  }
  #progress_bar.loading {
    opacity: 1.0;
  }
  #progress_bar .percent {
    background-color: #99ccff;
    height: auto;
    width: 0;
  }
</style>

<input type="file" id="files" name="file" />
<button onclick="abortRead();">Cancel read</button>
<div id="progress_bar"><div class="percent">0%</div></div>

<script>
  var reader;
  var progress = document.querySelector('.percent');

  function abortRead() {
    reader.abort();
  }

  function errorHandler(evt) {
    switch(evt.target.error.code) {
      case evt.target.error.NOT_FOUND_ERR:
        alert('File Not Found!');
        break;
      case evt.target.error.NOT_READABLE_ERR:
        alert('File is not readable');
        break;
      case evt.target.error.ABORT_ERR:
        break; // noop
      default:
        alert('An error occurred reading this file.');
    };
  }

  function updateProgress(evt) {
    // evt is an ProgressEvent.
    if (evt.lengthComputable) {
      var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
      // Increase the progress bar length.
      if (percentLoaded < 100) {
        progress.style.width = percentLoaded + '%';
        progress.textContent = percentLoaded + '%';
      }
    }
  }

  function handleFileSelect(evt) {
    // Reset progress indicator on new file selection.
    progress.style.width = '0%';
    progress.textContent = '0%';

    reader = new FileReader();
    reader.onerror = errorHandler;
    reader.onprogress = updateProgress;
    reader.onabort = function(e) {
      alert('File read cancelled');
    };
    reader.onloadstart = function(e) {
      document.getElementById('progress_bar').className = 'loading';
    };
    reader.onload = function(e) {
      // Ensure that the progress bar displays 100% at the end.
      progress.style.width = '100%';
      progress.textContent = '100%';
      setTimeout("document.getElementById('progress_bar').className='';", 2000);
    }

    // Read in the image file as a binary string.
    reader.readAsBinaryString(evt.target.files[0]);
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

Ejemplo: control del progreso de una lectura. ¡Haz una prueba!

0%

Sugerencia: para ver cómo funciona este indicador de progreso, intenta utilizar un archivo grande o un recurso de una unidad remota.

Referencias

Comments

0