Upload de archivos con barra de progreso usando XMLHttpRequest y la nueva API de HTML5

Posted on Febrero 15th, 2010 in AJAX, Desarrollo Web, Javascript | 3 Comments »

Creo que para muchos ha sido un desafío implementar un upload de archivos con barra de progreso ya que se debía enviar el formulario a un iframe oculto y mientras tanto revisar el progreso haciendo peticiones AJAX en pequeños intervalos. A esto había que sumarle todo el proceso del lado del servidor para calcular el porcentaje, y no todos los lenguajes nos proveen esa información.

Si bien la posibilidad de leer archivos ya existía en Firefox 3, combinado con las nuevas características de la última versión de este navegador, se pueden implementar funcionalidades mas avanzadas.

Con la nueva API podemos leer archivos del lado del cliente y de este modo enviar el contenido binario mediante una petición usando el nuevo método sendAsBinary() del objeto  XMLHttpRequest.

Para acceder a los archivos, tenemos la propiedad files disponible en los elementos input de tipo file y ademas en el objeto DataTransfer (accesible en las operaciones de drag & drop). Esta propiedad es un objeto de tipo FileList que representa una colección de objetos de tipo File. Un objeto File dispone de propiedades que proveen información básica del archivo, como el nombre, el tipo y el tamaño.

Gecko 1.9.2 imlementa el objeto FileReader, el cual nos permite leer archivos en forma asincrónica y asi evitar que el navegador no responda durante la operación de lectura.

Si bien tenemos la posibilidad de enviar el contenido de un archivo como binario, para enviar múltiples archivos debemos generar el POST tal cual lo haría un formulario con el atributo enctype="multipart/form-data".

Pueden ver en acción un ejemplo muy sencillo – basado en jQuery – que permite seleccionar multiples archivos y enviarlos en una sola petición. Se puede ver el progreso mediante una barra. Del lado del server, con PHP hago un print_r() con el contenido de la variable $_FILES. Lo pueden ver con el Firebug.

Veamos paso a paso el código Javascript del ejemplo.

upload = [];

j(function() {
	j('#upload-progressbar').progressbar();

	j('#files').bind('change', function(e) {
		var files = e.target.files;
		var fileCount = 0;
		j.each(files, function(k, v) {
			fileCount++;
			reader = new FileReader();
			reader.onloadend = function(e) {
				upload.push({
					name : v.name,
					type : v.type || 'text/plain',
					bin : e.target.result // el contenido del archivo
				});
				if (upload.length == fileCount) uploadFiles();
			};
			reader.readAsBinaryString(v);
		});
	});
});

Al cargar la página, agregamos el evento change para el input. Cuando el usuario selecciona los archivos, iteramos por cada uno de ellos y leemos su contenido usando el metodo readAsBinaryString() del objeto FileReader. Al ser asincrónico, debemos utilizar el evento onloadend para detectar cuando terminó la operación. Dentro de esta función creamos un objeto con algunas propiedades del archivo y guardamos una referencia en un array global. Cuando detectamos que se han leído todos los archivos, procedemos a enviar el formulario:

function uploadFiles() {
	var xhr = new XMLHttpRequest();

	j('#upload-progressbar').show();

	xhr.upload.addEventListener('progress', function(e) {
		if (e.lengthComputable) {
			var perc = Math.round((e.loaded * 100) / e.total);
			j('#upload-progressbar').progressbar('value', perc);
		}
	}, false);
	xhr.upload.addEventListener('load', function(e) {
		j('#upload-progressbar').progressbar('value', 100);
	}, false);

	xhr.open("POST", '/es/demos/ajax-upload');

	var BOUNDARY = '---------------------------1966284435497298061834782736';
	var rn = "\r\n";
	var req = '';
	j.each(upload, function(k, v) {
		req += "--" + BOUNDARY + rn + "Content-Disposition: form-data; name=\"files[]\"";
		req += '; filename="' + v.name + '"' + rn + 'Content-type: ' + v.type;
		req += rn + rn + v.bin + rn;
	});
	req += "--" + BOUNDARY + '--';

	xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
	xhr.sendAsBinary(req);
}

Creamos un objeto XMLHttpRequest. Aquí vemos que xhr.upload hace referencia a un objeto que contiene información sobre el proceso de upload y nos ofrece algunos eventos útiles. Abrimos una conección para enviar el formulario por POST y luego generamos el contenido binario que recibiremos en el servidor. Debemos establecer que el Content-Type es de tipo multipart/form-data, y como indica el nombre, quiere decir que la petición esta formada por múltiples partes. Para separarlas se utiliza una cadena que no aparezca dentro del contenido de los archivos (si estan aburridos pueden leer mas detalles aquí). En este caso es la variable BOUNDARY. Finalmente, la magia ocurre en el método sendAsBinary(). Pueden ver mas detalladamente el contenido del POST con el Firebug.

Esta técnica nos permite, por ejemplo, hacer una vista previa de las imágenes en el cliente o redimensionarlas antes de subirlas.

Quizas piensen en la seguridad como yo e intenten hacer esto:

j('#files').val('/etc/passwd');

Pero desgraciadamente afortunadamente nos encontramos con un error de seguridad :)

No he investigado mucho en cuanto a compatibilidad en distintos navegadores. Si alguien tiene información al respecto es bienvenida.

UPDATE: El comportamiento en Firefox 3.6.2 sobre Ubuntu es algo inestable. A veces envía la petición pero se queda esperando una respuesta indefinidamente, y al seleccionar varios archivos se cierra el navegador de forma inesperada.

  • Share/Bookmark

Geolocalizando al usuario

Posted on Julio 14th, 2009 in Desarrollo Web, Javascript | 2 Comments »

Una de las nuevas características de los navegadores actuales que introdujeron soporte para HTML5 es la API de Geolocalización, la cual nos permite obtener las coordenadas de la posición actual del usuario. Actualmente solo Firefox 3.5  y Opera 10 beta lo implementan en forma nativa, pero el servicio también está disponible instalando Gears.

La API hace transparente al programador la forma en que se obtiene la posición del usuario. Hay varias formas de determinarla y éstas dependen de la plataforma y el dispositivo del usuario. Si se accede desde un dispositivo con GPS, se podrá obtener las coordenadas en forma casi exacta. Si se accede desde una PC o móvil sin GPS, el navegador envía una solicitud al servicio de geolocalización de Google con información sobre los puntos de acceso WiFi cercanos, aunque en este último, la exactitud de la posición obtenida deja mucho que desear en algunos casos.

A continuación les dejo un código de prueba que usé para determinar la localización con Firefox 3.5.
Irónicamente, a pesar de estar ubicado en el barrio de Recoleta, las coordenadas resultantes me ubican en Puerto Madero a metros de las oficinas de Google.

if (navigator.geolocation) {
    // nos aseguramos de que el browser soporte la API
    navigator.geolocation.getCurrentPosition(function(position) {
    	var latitude = position.coords.latitude;
    	var longitude = position.coords.longitude;
        // aca podemos ubicar el punto en un mapa
        // o hacer una geocodificación inversa para obtener la dirección
    }, function(error) {
	// error es un objeto con las siguientes propiedades:
    	// - code: código de error
    	// - message: mensaje de error
    });
}

Al método getCurrentPosition() se le envía como primer parámetro un callback que recibirá un objeto con las propiedades de la posición. El segundo parámetro es opcional y es un callback que se llamará si ocurre algún error en el proceso.

Si usamos la API de Gears, el proceso es el mismo, salvo que tenemos algunas opciones adicionales, incluso el objeto position ya nos devuelve la dirección luego de hacer una geocodificación inversa.

Enlaces útiles:

  • Share/Bookmark

¿Ya podemos usar HTML 5?

Posted on Junio 15th, 2009 in Desarrollo Web | 2 Comments »

Vamos a comentar algunas de las nuevas características de este nuevo lenguaje que pretende reemplazar al ya decrépito HTML 4.01. Esta nueva revisión del lenguaje nació hace unos años bajo el nombre Web Applications 1.0 y continúa siendo desarrollado activamente por el grupo WHATWG como HTML5, y además su otra variante XHTML5. Se dice que estará completo para el 2012, pero la mayoría de los navegadores actuales ya implementan gran parte de las nuevas características del lenguaje.

Algunas de las mejoras que nos provee HTML5 son las siguientes:

  • Nuevos tags como <header>, <footer>, <section>, etc.  En su mayoría son reemplazos semánticos para los bloques genéricos como <div> o <span>.
  • La incorporación de Web Forms 2.0, una actualización para el manejo de formularios que agrega nuevos tipos de datos y facilidad para la validación de campos.
  • Se eliminaron elementos como <font> y <center>, cuya funcionalidad puede ser reemplazada por CSS.
  • Grandes cambios en la API del DOM, incluyendo soporte para dibujar en 2D usando el nuevo elemento <canvas>, soporte para aplicaciones offline, Drag & Drop, controles de reproducción multimedia para usar con los elementos <audio> y <video>, manejo avanzado del historial y mucho mas.

HTML5 nos dá la opción de crear el documento usando la vieja sintaxis compatible con HTML 4 u optar por un documento XML.

Ejemplo para la primera opción:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Documento de ejemplo</title>
  </head>
  <body>
    <p><img src="/images/logo.png" alt="" /> Hola mundo</p>
  </body>
</html>

Datos importantes:

  • El documento se sirve como Content-Type: text/html
  • El DOCTYPE  es requerido y no necesita referirse a ningún DTD.
  • Nueva forma de especificar el juego de caracteres mediante el atributo charset del <meta>.
  • Por cuestiones de compatibilidad, se permite cerrar los tags vacíos como <img>, <input> o <br> con una barra, usando la misma sintaxis que XML.
  • Ventaja: compatible con navegadores que no soportan HTML5

Ejemplo conforme a la sintaxis XML:

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Documento de ejemplo</title>
  </head>
  <body>
    <p><img src="/images/logo.png" alt="" /> Hola mundo</p>
  </body>
</html>

Datos importantes:

  • El documento se sirve como Content-Type: application/xhtml+xml
  • El DOCTYPE  es opcional.
  • Se requiere definir el namespace como http://www.w3.org/1999/xhtml
  • Alcanza con especificar el charset en la definición del XML.
  • Ventaja: obliga a escribir código XML válido.

Uno de los grandes cambios que debemos tener en cuenta es la forma en que se sirve el documento. Hasta ahora estábamos acostumbrados a enviarle al navegador un documento XHTML como text/html por problemas con navegadores defectuosos como Internet Explorer ciertos navegadores. Pero esto hará que el navegador interprete erróneamente el documento XML como HTML.

Lo que no queda del todo claro es como va a ser la compatibilidad hacia atrás. No solo tendremos que lidiar con navegadores “prehistóricos” por un largo tiempo, sino que además se han introducido cambios en el significado de varios elementos como <strong>, <small>, <b>, <i>, que implicaría tener que reescribir parte de la estructura del documento.

Adicionalmente, ¿qué sucede si servimos un documento en HTML5 en un browser que no lo soporta? ¿Cómo se interpretan los nuevos elementos?
Uno de los problemas encontrados en Internet Explorer para todas sus versiones es la incapacidad de renderizar y darle estilo a los elementos desconocidos. Para esto se logró hallar un hack, el cual consiste en crear el elemento dińamicamente usando el método del DOM document.createElement() en el head del documento.

Les dejo algunos enlaces relevantes (en inglés):

  • Share/Bookmark