Mostrando entradas con la etiqueta AJAX. Mostrar todas las entradas
Mostrando entradas con la etiqueta AJAX. Mostrar todas las entradas

2011/04/16

Comunicaciones con HTML5

El post de hoy es muy interesante ya que nos va a permitir romper con las limitaciones tradicionales de las páginas web. No os lo perdáis...

Casi todas las páginas que hacemos, tienen la necesidad de comunicarse o desde o hacia el exterior. Normalmente usamos AJAX, pero no siempre es la solución óptima. Con HTML5, han aparecido varias tecnologías que nos ayudan a rellenar los huecos dejados por AJAX para realizar operaciones complejas de forma nativa.

Concretamente, vamos a ver 5 métodos de comunicación
  • XHR & XHR2 con CORS. XHR = XMLHttpRequest, es decir, AJAX, pero en este caso, con esteroides.
  • Web Messaging
  • Web Sockets
  • Server Sent Events
  • Web Workers
Todos ellos menos XHR están basados en eventos lo que hace que todo sea más dinámico y por supuesto, sin necesidad de recargar la página.

XHR & XHR2 con CORS
Parece un gazpacho de letras aleatorias... En cristiano es XMLHttpRequest y XMLHttpRequest2 con Cross Origin Resource Sharing. Básicamente de lo que se trata es que habilitar las peticiones AJAX entre distintos dominios al de la página actual, pues ya sabéis que por motivos de seguridad, ese tipo de peticiones están capadas (aunque algunos navegadores se lo pasen por el forro en el mejor de los casos... no te escondas IE, que se te ve el plumero).

El secreto para permitir peticiones entre distintos dominios reside en el servidor de destino, de modo que en la cabecera se especifica el dominio o dominios desde los que permiten realizar peticiones AJAX. También admite comodines. Un ejemplo en PHP es el siguiente:
<?php
header('Access-Control-Allow-Origin: *');
?>

Entonces, os preguntaréis de dónde viene el objeto XHR2. Básicamente, lo que ocurre es que el objeto XHR original no permite o no debe permitir abrir un URL de un dominio distinto al actual. Sin embargo el uso de un objeto u otro es transparente al programador, menos para IE8, de nuevo, que ha de usar XDomainRequest. Un ejemplo de llamada usando XHR2 con CORS a un dominio distinto al de la página actual es éste.
var client = new XMLHttpRequest();
client.onreadystatechange = function () {
  if (this.readyState == 4 && this.status == 200) {
    alert('The most awesome-est person to follow: ' + this.responseText);
  }
};
client.open('GET', 'http://remysharp.com/demo/cors.php');
client.send();
Web Messaging con postMessage
Esta tecnología es bastante antigua ya en cierta manera está soportada desde IE6.

El principal punto fuerte de esta tecnología es el permitir la comunicación entre páginas de distintos dominios y es una de las maneras más sencillas de saltarse la limitación de la política de mismo dominio. El principio de funcionamiento es el siguiente. En una página tenemos un contenido con un iframe que apunta a otra página distinta que es con la que nos queremos comunicar. La página principal dispone de un dódigo javascript que permite recibir eventos onmessage del tipo
window.onmessage = function (event) {
  if (event.origin == 'mytrustedsite.com') {
    alert('mi sitio de confianza ha dicho: ' + event.data);
  }
};
 El contenido del iframe además podrá ser originador de mensajes mediante el siguiente código.
// iframe es el nodo DOM del propio iframe
iframe.contentWindow.postMessage("hola", location.host);
La comunicación está limitada a cadenas de texto, pero siempre se pueden realizar conversiones de tipo JSON-string y string-JSON con las funciones JSON.stringify y JSON.parse.

Para aumentar la compatibilidad a IE6, hay un proyecto llamado EasyXDM que lo que hace es en función de la versión del navegador, si es necesario añadir un flash que permite el envío de messages. Para más información, en su sitio.

Web Sockets
Web sockets es la tecnología para mí con más futuro ya que permite no tener que recurrir a inventos artificiosos para comunicar una página web con cualquier cosa ya que usa sockets y eventos.

Los web sockets son equivalentes a los sockets de comunicación de toda la vida pero no son lo mismo. Para poder usar web sockets, se usa un protocolo especial ws:// de modo que es necesario que el servidor de destino soporte ese protocolo.

Hay muchas implementaciones de servidores que soportan el protocolo, pero desafortunadamente, los servidores normales no lo soportan de forma nativa. Esto es debido a que el modelo de funcionamiento basado en conexión como por ejemplo el de apache, no es compatible con el modelo de funcionamiento basado en eventos como pasa con los web sockets. Además, existe un paradigma denominado el problema de las 10000 conexiones C10k, que especifica que un servidor debe ser capaz de soportar 10000 conexiones simultáneas. El problema viene en que para que un servidor de tipo tradicional orientado a la conexión soporte websockets, ha de mantener la conexión abierta continuamente para mantener levantado el canal de comunicación entre la página y el servidor... esa conexión no se cierra y si hacemos un cálculo rápido de unas 50 conexiones por página, la frontera de las 10000 conexiones se nos puede hacer muy pequeña.

Aquí entran en juego los servidores orientados a eventos, que mediante una única conexión abierta por servicio nos permiten servir a un número ilimitado de peticiones. Es decir, que por cada tipo de recurso (llamémosla página) solo habrá que tener una conexión abierta. Así, un servidor ligero puede ser capaz de servir de forma muy rápida no solo a 10000 clientes simultáneos, sino a 100000, 200000 etc. Hay varias alternativas de servidores pero las que más me gustan son las siguientes:
  • Node. Mi favorito. Servidor ligero basado únicamente en javascript V8. Es sencillo de implementar, extremadamente rápido y muy muy ligero. Hay servidores que te permiten hospedar gratuitamente servidores node como es el caso de Joyent. Hay extensiones para node para acceso a bases de datos e integraciones con otros servicios para respuestas en tiempo real. Un ejemplo de servidor node es tan sencillo como el siguiente, que responde con una página web con la cadena Hello World a peticiones websocket hechas sobre el servidor:
var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(8124, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8124/');
otro ejemplo de servidor TCP con node que hace un echo de lo que se le envíe
var net = require('net');
var server = net.createServer(function (socket) {
  socket.write("Echo server\r\n");
  socket.pipe(socket);
})
server.listen(8124, "127.0.0.1");
  • WaterSpout. otro que también funciona muy bien. hecho en PHP y su principal ventaja es que soporta tanto web sockets como longpolling (tecnología sobre la que hablaré otro día). Evidentemente no es un PHP para ser servido por apache por ejemplo, si no para ser ejecutado desde línea de comandos y mantenerse en segundo plano. Nota para los losties, probad el chat multiusuario de John Locke ;)
  • Apache. Aunque apache no es la plataforma ideal, si se tiene el humor de leerse un par decientos de páginas de documentación y hacer cientos de cambios de parámetros de configuración, existe un módulo basado en pyton para apache para soportar websockets llamado pywebsockets. Más información aquí.
  • Nginx. Detrás de una web horrorosa que da pocas ganas de probarlo se esconde este servidor cuya principal virtud quizás es que dispone de binarios apra windows y que coge un 46% de la cuota de mercado de servidores de websockers (el 54% lo más probable es que sean node).
Bueno, ya tenemos los cimientos del edificio, ahora a por las paredes. Quién soporta websockets? pues todos los navegadores de verdad menos los sospechosos habituales (Internet Explorer, al rincón!) bueno, realmente, solo chrome y safari. Firefox tampoco lo soporta nativamente... pero realmente hay maneras de que desde el IE6 se puedan usar websockets mediante una librería que incorpora un flash que suple esta carencia a costa de menor rendimiento en la carga de la página debido a la carga del flash. El proyecto en cuestión se llama web-socket-js y solo se puede decir que funciona y es transparente, qué más se puede pedir?!. El desarrollador no asegura que en IE < 8 no haya errores, pero que directamente pasa de dar soporte para esas versiones aunque debería funcionar teniendo flash > v.10.

El tejado de nuestro edificio es el código dentro de la página y una vez tenemos toda la infraestructura montada es realmente sencillo... tan sencillo que da miedo...

var ws = new WebSocket('ws://somesite.com/updates');

ws.onmessage = function (event) {
  alert(event.data);
};

ws.onopen = function () {
  ws.send('yay! we connected!');
};
Lo más importante, es cambiar el "chip" de programación ya que los web sockets tanto en su parte servidora como en la cliente está 100% orientada a eventos y como tan, necesita que el código sea así ya que es la única manera de conseguir conexiones no bloquentes. Es un imperativo hacerlo así.

Server-Sent Events
Una tecnología que viene desde el 2006 promocionada por Opera. Es de un único sentido desde el servidor hacia el cliente y se usa para realizar push de información hacia los clientes además de implementar la limitación de mismo dominio. Soportada únicamente por Opera, Safari y Chrome, pero bajo mi punto de vista, la limitación de plataformas soportadas y el sentido único hace que no sea especialmente atractiva.

Hace falta un servidor preparado como pasa con los web sockets. Un ejemplo de servidor basado en Node es éste y en la parte cliente es prácticamente idéntico a web sockets, solo cambiando el objeto referenciado y el protocolo, claro.

var es = new EventSource('/sse');
es.onopen = function () {
  console.log('opened stream');
};

es.onmessage = function (event) {
  console.log('new message: ' + event.data);
};
Web Workers
En pocas palabras, un web worker es un javascript de una página que se ejecuta en segundo plano. Proporciona un sistema de comunicación bidireccional  de cadenas de texto con su página principal basado en mensajes. Evidentemente, no implementa "per se" comunicación cliente-servidor... solo de una página con un proceso en segundo plano creado por ella.

Ejemplo de creación de web worker en la página principal
var worker = new Worker('bigjob.js');
worker.onmessage = function (event) {
  alert('Message from worker: ' + event.data); // event.data es string!
};

worker.postMessage('task=job1');
Código fuente del worker bigjob.js.
this.onmessage = function (event) {
  var job = event.data;
  if (job == 'task=job1') {
    job1();
  } else if (job == 'task=job2') {
    job2();
  }
};

// just a pseudo example
function job1() {
  // do some big task
  while (working) {
    // continue task
    this.postMessage('job1 ' + amountComplete + '% complete');
  }
  this.postMessage('job1 100% complete');
}
Internet Explorer no soporta web workers para variar. Sin embargo, funcionan en Chrome, Safari, Opera y Firefox.

2011/04/11

Formularios con subidas

Me encanta AJAX, pero eso no quiere decir que tenga sus pegas. La principal y puede que la única, es que no soporta el envío de ficheros mediante POST, pero siempre hay maneras de saltarse las limitaciones con más o menos arte y además, sin tener que preocuparse de interfaces extrañas ya que es equivalente a la usada con jQuery para las llamadas AJAX. Vamos a ello.

El primer paso es saber cómo se hacen las llamadas AJAX con jQuery (documentación aquí!). Hay varias maneras de hacer lo mismo, pero la mayoría de ellas son sinónimos para simplificar la interfaz:

$.ajax() Esta es la principal y la que hace todo. Básicamente, se pasa como parámetro un objeto con los parámetros y los closures que se llamarán al generarse los eventos correspondientes. Un ejemplo de uso básico mediante POST es el siguiente, que llama a "some.php" mediante POST pasando como parámetros a la página "name" y "location" y que cuando todo va bien llama a una función que hace un alert del texto generado por some.php.
$.ajax({
   type: "POST",
   url: "some.php",
   data: "name=John&location=Boston",
   success: function(msg){
     alert( "Data Saved: " + msg );
   }
 });
Aquí va un listado de las propiedades que se le pueden pasar como parámetro

accepts
async    true
beforeSend(jqXHR, settings)
cache    true
complete(jqXHR, textStatus)
contents
contentType    'application/x-www-form-urlencoded'
context
converters    {"* text": window.String, "text html": true, "text json": jQuery.parseJSON, "text xml": jQuery.parseXML}
crossDomain    false
data
dataFilter(data, type)
dataType   Intelligent Guess (xml, json, script, or html)
error(jqXHR, textStatus, errorThrown)
global    true
headers    {}
ifModified    false
isLocal
jsonp
jsonpCallback
mimeType
password
processData    true
scriptCharset
statusCode    {}
success(data, textStatus, jqXHR)
timeout
traditional
type    'GET'
url    página actual
username
xhr
xhrFields
Como siempre, para más detalles la página de jQuery con la documentación.

Aparte de la función $.ajax(), hay varios sinónimos que dan por sentado ciertos parámetros. Los principales son los siguientes y para más detalles mirad aquí
  • $.get( url, [ data ], [ success(data, textStatus, jqXHR) ], [ dataType ] )
  • $.getJSON( url, [ data ], [ success(data, textStatus, jqXHR) ] )
  • $.getScript( url, [ success(data, textStatus) ] )
  • $.load( url, [ data ], [ complete(responseText, textStatus, XMLHttpRequest) ] )
  • $.post( url, [ data ], [ success(data, textStatus, jqXHR) ], [ dataType ] )
Ahora que ya tenemos una visión de conjunto podemos centrarnos en el problema, que es subir datos mediante un formulario. Se os puede dar el caso de que tenéis un maravilloso código que mediante AJAX hace lo que tiene que hacer pero de repente el cliente os pide que además de manejar el formulario tiene que subir un fichero adjunto. Normalmente habría que tirar a la basura todo lo hecho además de que la página tendría que ser dinámica para pasar a ser estática como se hacía hace 10 años... bueno, pues hay varias técnicas para resolverlo, pero lo más común es usar un iframe para realizar las operaciones de forma oculta sin tener que recargar la página prinicpal, pero volvemos a lo mismo, tenemos que reprogramar toda la lógica de negocio del envío y tratamiento de la información del formulario.

Aquí es donde nos ayuda este plugin de jQuery de Jaina Ewen que lo que hace es crear por nosotros la infraestructura del iframe y darnos una interfaz similar a la del método ajax de jQuery de modo que no tengamos que hacer mas que unos cambios mínimos en la página y el código.

Originalmente tendríamos un código tipo
<form id="miform"> ... </form>
Con una llamada AJAX parecida a
// Asigna el evento de envío de formulario
$('#miform').submit(enviaFormulario);

// ...

function enviaFormulario()
{
    // ... lee los valores del formulario en variables valor1, valor2, etc y otros cambios estéticos a la página ...
    $.ajax({
       type: "GET",
       url: "paginaAjax.php",
       data: "campo1=" + valor1 + "&campo2=" + valor2,
       success: function(data) {
         alert( "Datos enviados. Respuesta: " + data );
       }
    });
    return false;
}

Por lo tanto, los cambios serían:

1. Incluir el script. Es de cajón, pero a quién no se le ha olvidado alguna vez?
<script type="text/javascript" src="js/jquery.iframe-post-form.js"></script>
2.  Modificar el <form> para que sea capaz de subir ficheros. También básico, pero fácil de olvidar
<form id="miform" action="paginaAjax.php" method="post" enctype="multipart/form-data">
3.  Modificar el código para el tratamiento de ese formulario. De momento nos sobra el leer los valores de los campos del formulario para contruir la cadena del data. El método es POST sí o sí y el url viene dado por el propio tag del form. Donde antes era "success" ahora es "complete". Y con ésto ya estaría nuestro código funcionando como antes.
$('#miform').iframePostForm
({
    post : function ()
    {
        // Cambios estéticos al enviar el formulario
        // p.e. deshabilitar botones u otras cosas
    },
    complete: function(data)
    {
        alert( "Datos enviados. Respuesta: " + data );
    }
});
Pues ahora ya no hay excusas para que todas nuestras páginas sean dinámicas y además, sin usar flash.