Upload A File With HTML5 / Subir un archivo con HTML5

El objeto XMLHttpRequest

El objeto XMLHttpRequest ha sido actualizado varias veces desde que fue definido como parte del esfuerzo WHATWG’s HTML utilizando la tecnología de Microsoft; entonces tuvimos la especificación original XMLHttpRequest Level 1 como parte de la W3C, también tuvimos la especificación actualizada XMLHttpRequest Level 2, y ahora tenemos la últma version de este documento conocido como XMLHttpRequest Living Specification. Podemo resumir sus ventajas en los siguientes puntos:

  • Permite subir y bajar archivos como flujo de bytes (stream bytes), archivos binarios de gran tamaño (BLOBs) o formularios de datos.
  • Tiene manejadores de eventos de progreso, errores, aborto, comienzo, y fin de operaciones.
  • Peticiones inter dominio (cross-domain or CORS)
  • Nuevos tipos de respuestas para JSON
  • Es parte fundamental de la HTML5 File API specification

Es importante recalcar que antes de HTML5 y la nueva versión del objeto XMLHttpRequest se requería recurrir a tecnología de lado servidor para poder realizar una operación que permitiera subir un archivo, es decir no era posible subir un archivo nativamente del lado cliente. Tecnologías como AJAX  y Flash hacían lo propio para tratar de hacer esto posible pero con serias limitaciones, por lo que XMLHttpRequest viene a cubrir este antiguo problema de gran manera. Existen otras características adicionales que acomapañan XMLHttpRequest  Level 2 , si desea conocer más puede recurrir a  la especificación oficial.

La mayoría de las veces utilizo código y comentarios en inglés, espero que esto no le resulte demasiado inconveniente, si tiene alguna duda la contestaré a la brevedad.

Comenzando

Lo primero que haremos es definir la interfaz de usuario para esta pequeña implementación comenzando por las etiquetas HTML, el código es muy sencillo y solo contempla algunos elementos de formulario, y algunos div que solo sirven para dar una mejor presentación auxiliandose de CSS3. No analizaré en este post lo que respecta a las hojas de estilo utilizadas ya que no es algo necesario para el funcionamiento del ejemplo.

<!DOCTYPE html>
<html>
 <head>
 <title>Upload File</title>
 <meta charset="iso-8859-1" />
 </head>
 <body>
<div id="wrap">
<div class="field">
<ul class="options">
 <li>
 <input type="file" id="myfile" name="myfile" class="rm-input" onchange="selectedFile();"/></li>
 <li>
<div id="fileSize"></div></li>
 <li>
<div id="fileType"></div></li>
 <li>
 <input type="button" value="Subir Archivo" onClick="uploadFile()" class="rm-button" /></li>
</ul>
</div>
<progress id="progressBar" value="0" max="100" class="rm-progress"></progress>
<div id="percentageCalc"></div>
</div>
</body>
</html>

El código anterior se explica casi por si solo, pero resumamos lo que tiene:

  • Un input de tipo file que servirá para seleccionar el archivo que se desa subir.
  • Un div que servirá para imprimir el tamaño del archivo seleccionado.
  • Un div que servirá para imprimir el tipo MIME del archivo seleccionado.
  • Un botón que disparará el proceso para subir el archivo elegido.
  • Una barra que indicará el progreso en el proceso de subida del archivo (nuevo elemento HTML5).
  • Finalmente un div donde se mostrará el progreso en formato de porcentaje.

La función selectedFile()

Cada vez que selecciona un archivo con el elemento file,  también dispara al evento onchange el cual llama a la función selectedFile(). En esta función suceden cosas interesantes, para empezar se hace referencia a una colección de archivos instanciada por un objeto nuevo en HTML5 llamado FileList, los objetos que obtenemos como miembros de las lista de FilesList son objetos File. En este caso obtendremos las propiedades size y type desde el objeto File recuperado.

Aprovenchando la información que proporciona la propiedad size, dentro de la función se calcula y se muestra en megabytes o kilobytes el tamaño del archivo que se ha seleccionado. Con la propiedad type se obtiene el tipo MIME del archivo seleccionado, el cual se muestra en el div correspondiente.

function selectedFile() {
var archivoSeleccionado = document.getElementById("myfile");
var file = archivoSeleccionado.files[0];
   if (file) {
       var fileSize = 0;
       if (file.size > 1048576)
          fileSize = (Math.round(file.size * 100 / 1048576) / 100).toString() + ' MB';
       else
          fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + ' Kb';

       var divfileSize = document.getElementById('fileSize');
       var divfileType = document.getElementById('fileType');
       divfileSize.innerHTML = 'Tamaño: ' + fileSize;
       divfileType.innerHTML = 'Tipo: ' + file.type;

    }
}

La función uploadFile()

Esta es la función que hace un mayor uso de las nuevas posibilidades de XMLHttpRequest , y es la que se encargará de disparar el proceso principal del lado cliente.

function uploadFile(){
 //var url = "/ReadMoveWebServices/WSUploadFile.asmx/UploadFile";
 var url = "/ReadMoveWebSite/UploadMinimal.aspx";
 var archivoSeleccionado = document.getElementById("myfile");
 var file = archivoSeleccionado.files[0];
 var fd = new FormData();
 fd.append("archivo", file);
 var xmlHTTP = new XMLHttpRequest();
 //xmlHTTP.upload.addEventListener("loadstart", loadStartFunction, false);
 xmlHTTP.upload.addEventListener("progress", progressFunction, false);
 xmlHTTP.addEventListener("load", transferCompleteFunction, false);
 xmlHTTP.addEventListener("error", uploadFailed, false);
 xmlHTTP.addEventListener("abort", uploadCanceled, false);
 xmlHTTP.open("POST", url, true);
 //xmlHTTP.setRequestHeader('book_id','10');
 xmlHTTP.send(fd);
}

Inicialmente tenemos una variable url que usaremos para indicar donde está la página o servicio web que recibirá la petición de esta página para hacer el proceso indicado en el servidor; enseguida tal y como se hizo en la funcion selectedFile() se hace referencia al objeto File miembro FileList obtenido.

En la cuarta línea hay algo novedoso y muy útil, me refiero al objeto FormData,  este objeto permite instanciar vía JavaScript un formulario web, es decir, es como si usted colocara con etiquetas HTML un formulario, o bien puede hacer referencia a uno ya existente asignándolo a un objeto FormData. Sin duada esto es de gran ayuda ya que significa que ahora usted puede crear un formulario y alterar los valores que envía de manera dinámica. Para adjuntar valores a un formualrio instanciado o referenciado con FormData se utiliza el método append(archivo, objeto), de esta manera en la quinta línea se agrega nuestro objeto File con el nombre de archivo.

Este es el fragmento de la función que abarca lo planteado:

//var url = "/ReadMoveWebServices/WSUploadFile.asmx/UploadFile";
var url = "/ReadMoveWebSite/UploadMinimal.aspx";
var archivoSeleccionado = document.getElementById("myfile");
var file = archivoSeleccionado.files[0];
var fd = new FormData();
fd.append("archivo", file);

Manejadores de eventos

Continuando con el resto de la función, podemos observar que finalmente instancía el objeto XMLHttpRequest  y se asigna a la variable xmlHTTP, enseguida procedemos a la siguiente novedad, me refiero a la posibilidad de crear los nuevos eventos que forman parte de XMLHttpRequest  Level 2 gracias al objeto upload. Los eventes que se agregan en este caso son:

  • loadstart. Evento que se dispara cuando inicia el proceso para subir el archivo.
  • progress. Evento que se dispara cada vez que hay un avance en el proceso que sube el archivo.
  • load. Evento que se dispara cuando la transferecia se completa.
  • error. Se dispara si el proceso falla con error explícito.
  • abort. Se dispara si el usuario interrumpe o la conexión de interrumpe.

No son los único eventos disponibles, consulte la especificación oficial para mayor información.

Los manejadores de eventos se declaran en el siguiente código:

var xmlHTTP= new XMLHttpRequest();
//xmlHTTP.upload.addEventListener("loadstart", loadStartFunction, false);
xmlHTTP.upload.addEventListener("progress", progressFunction, false);
xmlHTTP.addEventListener("load", transferCompleteFunction, false);
xmlHTTP.addEventListener("error", uploadFailed, false);
xmlHTTP.addEventListener("abort", uploadCanceled, false);

Las funciones que se llaman en cada evento son las siguientes:

function progressFunction(evt){
    var progressBar = document.getElementById("progressBar");
    var percentageDiv = document.getElementById("percentageCalc");
    if (evt.lengthComputable) {
        progressBar.max = evt.total;
        progressBar.value = evt.loaded;
        percentageDiv.innerHTML = Math.round(evt.loaded / evt.total * 100) + "%";
    }
}
 
function loadStartFunction(evt){
    alert('Comenzando a subir el archivo');
}
 
function transferCompleteFunction(evt){
    alert('Transferencia completa');
    var progressBar = document.getElementById("progressBar");
    var percentageDiv = document.getElementById("percentageCalc");
    progressBar.value = 100;
    percentageDiv.innerHTML = "100%";
}
 
function uploadFailed(evt) {
    alert("Hubo un error al subir el archivo.");
}
 
function uploadCanceled(evt) {
    alert("La operación se canceló o la conexión fue interrunpida.");
}

La función progressFunction es la que actualiza tanto la barra de estado como el porcentaje que indican de manera gráfica y numérica el avance del proceso, el resto de las funciones únicamente despliegan un mensaje apropiado para el caso.

Código comentado

Si ha observado el código presentado habrá notado algunas líneas comentadas, eso es debido a que este es el código base para hacer algo un poco más complejo, pero decidí dejar esas líneas porque pueden ser una referencia útil para alguien:

//var url = "/ReadMoveWebServices/WSUploadFile.asmx/UploadFile";

La línea anterior es una llamada a un servicio HTTP .Net en lugar de a una página.

//xmlHTTP.upload.addEventListener("loadstart", loadStartFunction, false);

Esta línea llama a una función que muestra un mensaje cuando inicia el proceso, la cual comenté porque después de ejecuar varias veces el código me pareció molesto.

El código completo

Así luce la implementación completa del código, no describo el código CSS3 ya que es irrelevante en lo que respecta a la funcionalidad, pero comparto una imágen que muestra como se ve ejecutándose:

<!DOCTYPE html>
<html>
    <head>
        <title>Upload File</title>
        <meta charset="iso-8859-1" />
                                    <link rel="stylesheet" type="text/css" href="estilosUploadFile.css" />
        <script type="text/javascript">
 
              function selectedFile() {
                var archivoSeleccionado = document.getElementById("myfile");
                var file = archivoSeleccionado.files[0];
                if (file) {
                    var fileSize = 0;
                    if (file.size > 1048576)
                        fileSize = (Math.round(file.size * 100 / 1048576) / 100).toString() + ' MB';
                    else
                        fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + ' Kb';
 
                    var divfileSize = document.getElementById('fileSize');
                    var divfileType = document.getElementById('fileType');
                    divfileSize.innerHTML = 'Tamaño: ' + fileSize;
                    divfileType.innerHTML = 'Tipo: ' + file.type;
 
                }
              }     
 
            function uploadFile(){
                //var url = "http://localhost/ReadMoveWebServices/WSUploadFile.asmx?op=UploadFile";
                var url = "/ReadMoveWebServices/WSUploadFile.asmx/UploadFile";
                var archivoSeleccionado = document.getElementById("myfile");
                var file = archivoSeleccionado.files[0];
                var fd = new FormData();
                fd.append("archivo", file);
                var xmlHTTP= new XMLHttpRequest();
                //xmlHTTP.upload.addEventListener("loadstart", loadStartFunction, false);
                xmlHTTP.upload.addEventListener("progress", progressFunction, false);
                xmlHTTP.addEventListener("load", transferCompleteFunction, false);
                xmlHTTP.addEventListener("error", uploadFailed, false);
                xmlHTTP.addEventListener("abort", uploadCanceled, false);
                xmlHTTP.open("POST", url, true);
                //xmlHTTP.setRequestHeader('book_id','10');
                xmlHTTP.send(fd);
            }       
 
            function progressFunction(evt){
                var progressBar = document.getElementById("progressBar");
                var percentageDiv = document.getElementById("percentageCalc");
                if (evt.lengthComputable) {
                    progressBar.max = evt.total;
                    progressBar.value = evt.loaded;
                    percentageDiv.innerHTML = Math.round(evt.loaded / evt.total * 100) + "%";
                }
            }
 
            function loadStartFunction(evt){
                alert('Comenzando a subir el archivo');
            }
            function transferCompleteFunction(evt){
                alert('Transferencia completa');
                var progressBar = document.getElementById("progressBar");
                var percentageDiv = document.getElementById("percentageCalc");
                progressBar.value = 100;
                percentageDiv.innerHTML = "100%";
            }   
 
            function uploadFailed(evt) {
                alert("Hubo un error al subir el archivo.");
            }
 
            function uploadCanceled(evt) {
                alert("La operación se canceló o la conexión fue interrunpida.");
            }
 
        </script>
    </head>
    <body>
        <div id="wrap">
            <div class="field">
                <ul class="options">
                    <li>
                        <input type="file" id="myfile" name="myfile" class="rm-input" onchange="selectedFile();"/>
                    </li>
                    <li>
                        <div id="fileSize"></div></li>
                    <li>
                        <div id="fileType"></div></li>
                    <li>
                        <input type="button" value="Subir Archivo" onClick="uploadFile()" class="rm-button" /></li>
                </ul>
            </div>
            <progress id="progressBar" value="0" max="100" class="rm-progress"></progress>
            <div id="percentageCalc"></div>
        </div>
    </body>
</html>

No descibo el CSS3 porque es irrelevante en términos de funcionalidad, pero comparto una imagen que muestra como luce la implementación en el navegador y un enlace al CSS3 estilosUploadFile.zip.

También comparto el HTTP service que utilicé para este ejemplo (el código de lado servidor, archivo de backend o cualquier otro nombre que ud. prefiera 😃) pero esto no será de mucha ayuda amenos que utilice exactemente el mismo stack que yo utilicé en su momento. En otras palabras, si usted es el tipo de personas que solo quiera copiar y pegar….jejeje bueno…quizá no está listo para hace esto todavía. Aquí está el tan solicitado archivo WSUploadFile.zip

Es todo por ahora amigos, espero que esto les sea de utilidad.

Algunos buenos libros que pueden ayudarle en su viaje por HTML5:

Twitter
Visit Us
Follow Me
YouTube
YouTube
LinkedIn
Share
Follow by Email
RSS