====== AJAX ======
Módulo perteneciente al curso [[informatica:programacion:cursos:programacion_avanzada_javascript|Programación avanzada con JavaScript y ECMAScript]].
===== Introducción =====
Desde sus comienzos y hasta hace relativamente poco tiempo las interfaces de usuario de las aplicaciones Web fueron más o menos siempre iguales. Las **limitaciones propias del protocolo HTTP** (//Hyper Text Transfer Protocol//) utilizado en las páginas Web han impuesto el tradicional **modelo de "Petición-Respuesta-Procesado en el navegador"** (a partir de ahora PRP) y vuelta a empezar.
Los pasos que sigue una aplicación Web convencional para funcionar suelen ser los siguientes:
- El usuario solicita una página.
- El servidor devuelve el contenido HTML correspondiente a ésta, normalmente generado a partir de alguna tecnología de servidor (como ASP.NET o PHP).
- El navegador recibe este HTML, lo procesa y visualiza el contenido resultante.
- El usuario interactúa con el HTML, envía un formulario al servidor o pulsa un enlace, y se repite el ciclo desde el paso 1: se solicita la página, se devuelve su contenido, se procesa y se visualiza.
Este proceso es el más natural para HTTP que estaba pensado para funcionar así cuando se concibió. El problema es que cualquier acción en la página que requiera apoyo del servidor implica solicitar otra página completa, descargarla y procesarla, lo cual es percibido de manera evidente por los usuarios (es decir "se nota" el refresco de la página) y crea una sensación poco amigable.
Además, si el retorno de la página tarda más que unos pocos milisegundos se pierde capacidad de respuesta de la interfaz puesto que, durante el proceso de petición-respuesta-procesado, la zona de visualización del navegador no responde.
La tendencia actual en todos los desarrollos web, sin embargo, es crear **aplicaciones y páginas cada vez más parecidas a aplicaciones de escritorio**, desdibujando las fronteras entre la Web y los programas que se ejecutan en el equipo local. Esto implica que los molestos y a la vez inevitables viajes al servidor no deberían ser percibidos por los usuarios y que la interfaz de la página debe responder en todo momento, jamás bloquearse. La sensación para los usuarios debe ser la de que la aplicación está todo el tiempo en su equipo, dejando de lado al servidor, como en una aplicación de escritorio tradicional. Seguro que has utilizado alguna vez GMail, Outlook.com, Facebook o alguna otra aplicación web reciente, así que sabes perfectamente de qué estoy hablando.
{{ :informatica:programacion:cursos:programacion_avanzada_javascript:08-ajax.jpg |}}
Ya hemos aprendido a lo largo de este curso las posibilidades que ofrecen los navegadores actuales para trabajar en el lado cliente con HTML, CSS y JavaScript. La tendencia imparable en las aplicaciones web es la de **llevar cada vez más procesamiento y lógica al navegador**, dejando el servidor para procesar las reglas de negocio y el acceso a datos.
Dado que las páginas se recargan cada vez menos pero las aplicaciones web necesitan comunicarse con el servidor, se necesita algún mecanismo para poder hacerlo de manera transparente al usuario. Ese mecanismo se denomina de manera genérica **AJAX**.
Este simpático acrónimo hasta hace poco asociado con el mundo de la limpieza y el fútbol, viene del acrónimo en inglés //Asynchronous JavaScript And XML//. Se basa en el uso de un objeto llamado **XMLHttpRequest** o abreviadamente **XHR** que está presente en todos los navegadores modernos. Como es fácil imaginar por su nombre, sirve para **realizar peticiones de documentos XML al servidor a través del protocolo HTTP**. La idea original era que, utilizando este objeto se solicitan al servidor datos en formato XML que, una vez recibidos en el navegador, es posible manipular mediante código JavaScript, mostrando los resultados al modificar dinámicamente los elementos de la página. Como comprobaremos en breve, desde este punto inicial a lo que AJAX implica actualmente, las cosas han cambiado mucho.
Ejemplo de auto-completar dinámicamente desde el servidor
con AJAX Selecciona un elemento de la primera lista y verás como se rellena
La anterior página web es un formulario con dos selectores. Lo que haces es que cuando se elige algo en el primer selector, en la segunda lista aparecerán más opciones dependiendo de lo que hayamos elegido.
Para la segunda lista de opciones, se hace una consulta usando AJAX a un script hecho en ASP.
El fichero ASP:
dinámicamente el contenido de la segunda con AJAX desde el servidor:
<%
Dim items
Select Case LCase(Request("tipo"))
Case "revistas"
items = Array("PC World", "MSDN Magazine", "CodeProject")
Case "blogs"
items = Array("www.campusmvp.com", "www.jasoft.org", "jmalarcon.es")
Case "empresas"
items = Array("Krasis [www.krasis.com]", "Microsoft [www.microsoft.com]", "Plain Concepts [www.plainconcepts.com]")
Case "libros"
items = Array("Crimen y castigo", "Cien años de soledad", "El Quijote")
End Select
'Se devuelve como XML
Response.ContentType = "text/xml"
'Se evita que haya caché de este contenido devuelto
Response.AddHeader "cache-control", "no-cache"
Response.Expires = -1
'Se devuelve el XML
Response.Write DevuelveMatriz(items)
'Esta función se encarga decrear el XML apropiado
Function DevuelveMatriz(arrItems)
Dim sRes
sRes = "" & vbCrLf & "
===== Envío de datos al servidor =====
Normalmente cuando pensamos en AJAX, es decir, en llamadas asíncronas a servicios, lo hacemos desde el punto de vista de obtener información: llamo a una página que me devuelve unos valores y los muestro en la interfaz de usuario. Es lo que acabamos de ver en el vídeo anterior.
Aunque este es el uso que primero viene a la mente para AJAX, lo cierto es que se utiliza tanto o más en el sentido inverso: **para enviar datos al servidor**. Con eso podemos actualizar un registro, enviar una orden, ordenar el borrado de un dato, o incluso subir un archivo.
La forma más sencilla y directa de enviar datos simples al servidor es incluirlos en la URL a la que llamamos como parámetros GET:
urldestino.aspx?Parametro1=1234&Parametro2=5
http = getHttpRequest()
http.onreadystatechange = finCarga;
http.open("POST", "http://www.miserv.com/misdatos.aspx", true)
http.send('Parametro1=1234&Parametro2=5');
Con esto no hemos ganado demasiado. Ahora se envían los datos por POST (sólo cambia el primer parámetro de open) pero **los hemos tenido que introducir en el método ''send'' en lugar de en la propia URL**. Esto sólo simularía el envío de parámetros mediante POST desde un formulario HTML, aunque por otro lado en ocasiones puede ser lo que queramos.
Lo habitual sin embargo es que, **en lugar de enviar parámetros, queramos enviar información arbitraria** del tamaño que sea preciso, que es para lo que suele usarse POST (por ejemplo, un objeto JSON completo con información). Esto se puede conseguir modificando ligeramente el código anterior para **incluir una cabecera** que indique al servidor que lo que le llega son, precisamente, datos (línea 3 del siguiente fragmento alternativo):
http = getHttpRequest()
http.onreadystatechange = finCarga;
http.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
http.open("POST", "http://www.miserv.com/misdatos.aspx", true)
http.send('Aquí ahora mando la información que quiera al servidor');
De esta manera podremos enviar información en formato JSON (luego lo veremos), los campos serializados de un formulario, o los contenidos de un archivo.
===== Problemas típicos de AJAX y cómo resolverlos =====
Ahora que ya conocemos los rudimentos de AJAX vamos a ver **cuáles son los principales problemas** que nos podemos encontrar al usar estas técnicas, y que en ocasiones pueden ser complicados de detectar.
OJO: Son los mismos problemas que nos encontraremos si utilizamos jQuery o alguna de las otras bibliotecas mencionadas, por lo que debemos ser conscientes de ellos.
Los más importantes son los siguientes:
* Llamadas fuera del dominio.
* Llamadas que producen errores o que no vuelven jamás.
* Contenidos no actualizados debido a cachés.
Las bibliotecas como jQuery nos simplifican mucho el uso de AJAX ya que tienen en cuenta todos estos factores y algunos más.
==== 1.- Llamadas fuera de dominio ====
Una vez que uno empieza a juguetear con las posibilidades de AJAX enseguida se nos ocurren ideas geniales para sacarle partido.
La más obvia, claro está, es la de **utilizar las técnicas para acceder desde el cliente a ciertos Servicios Web de utilidad ajenos** ubicados en Internet. Así, dado que los Servicios Web están basados en XML o JSON, es muy fácil procesar lo que devuelven con las técnicas descritas para, por ejemplo, realizar búsquedas en Amazon con su API, seguir una subasta en eBay, enviar "posts" a nuestro blog, consumir fuentes RSS, etc...
Todo esto es estupendo pero tiene un gravísimo problema: **los navegadores, por cuestiones de seguridad, bloquean todas las peticiones realizadas mediante XmlHttpRequest a dominios que no sean el que aloja la página desde la que se está usando**.
En realidad se trata de una restricción bastante lógica y que aparece en otras partes del navegador como ya hemos visto en otros módulos. Pero esto, claro está, **supone una limitación importante** para ciertos tipos de aplicaciones AJAX que podríamos desarrollar, como las de los ejemplos comentados.
La pregunta ahora es entonces: ¿Cómo solventamos esta situación?
En Internet Explorer basta con bajar el nivel de seguridad para que ya funcione correctamente, pero no es una buena solución (no le puedes pedir esto a tus usuarios). En otros navegadores (Firefox, Opera, Chrome y Safari) no hay forma de saltarse esta restricción. Existe una salvedad en Firefox que consiste en firmar digitalmente el JavaScript que usas, pero tampoco vale de mucho pues sólo funcionaría en este navegador.
La única forma de solucionarlo de manera independiente al navegador es, aunque sea obvio, hacer que no dependa de éste. Es decir, **llevarnos el problema al servidor**, donde estas restricciones no aplican. Para ello lo que debemos hacer es **construir un servicio proxy** que actúe de intermediario en nuestro servidor (al que sí podremos llamar con AJAX) y que sea éste el que se encargue de realizar la llamada a otros dominios devolviendo el resultado a nuestro JavaScript (directamente o preprocesándolo de algún modo).
Si construyes un servicio como este ¡ten mucho cuidado!. Normalmente este tipo de servicios -al igual que los que se encargan de leer archivos de disco de manera genérica y otros similares- son un verdadero peligro de seguridad si no los programamos bien. Si optas por esta solución lo mejor es que tomes varias precauciones de cara a la seguridad: **tener muy acotados los servicios o URLs a los que se puede llamar desde el proxy**. Lo mejor es identificarlos a cada uno con un número o código decidiendo a cuál se llama, pero nunca poniendo la URL directamente en la llamada desde JavaScript.
Otra solución, mucho más apropiada y de uso bastante extendido en la actualidad, requiere la connivencia con el servidor al que queremos acceder. Se trata de **una técnica conocida como JSONP** que utiliza otra forma diferente de acceder a los resultados. La veremos en un apartado específico un poco más adelante.
==== 2.- Gestión de errores y llamadas que no vuelven ====
**No podemos asumir que las llamadas que hagamos al servidor van a funcionar siempre**. Puede haber un error en el código del servidor, es posible que haya cambiado la URL y que no exista la página que llamamos, que haya errores de permisos, etc... Lo que pase en el servidor está fuera de nuestro control. Ante eso hay que estar preparado. La forma de controlar estas situaciones es, como en cualquier componente de comunicaciones por HTTP, a través del código de estado que devuelva el servidor. Todo esto ya se había mostrado antes y se había tenido en cuenta en el código del manejador de fin de carga. Podríamos afinar más en el mensaje de error y devolver uno diferente según el código de estado.
Hay, sin embargo, una situación menos frecuente pero más peligrosa que se puede producir: que la llamada asíncrona al servidor no vuelva o no lo haga en un tiempo razonable, es decir **que se produzca lo que se denomina un //timeout//**. ¿Qué hacemos en ese caso?.
Lo que podemos hacer en estos casos es **establecer un temporizador con el tiempo máximo que deseemos esperar**, para que al cabo de ese tiempo la petición sea anulada directamente, sin esperar más que llegue la respuesta. Podemos verlo en este ejemplo:
var http = getHttpRequest()
http.onreadystatechange = finCarga;
http.open("GET", "misdatos.aspx", true)
var tmrAnular = setTimeout("AnularPeticion()", 20000); //20 segundos
http.send(null);
function AnularPeticion()
{
http.abort();
}
function finCarga()
{
if (http.readyState == 4) //4: completado
{
clearTimeOut(tmrAnular);
if (http.status == 200) //200: OK
{
res = http.responseXML;
Procesarespuesta();
}
else //Se produjo un error
{
alert("No se pudo recuperar la información: " + http.statusText);
}
}
}
Se ha modificado el código de llamada anterior para añadir la creación de un temporizador que se encarga de anular la petición al pasar un tiempo determinado (en este caso de 20 segundos, pero puede ajustarse a cualquier otro valor). Nótese también como en el evento de fin de carga eliminamos el temporizador (que ya no nos hace falta) cuando la petición termina de procesarse, en caso de que regrese.
==== 3.- Contenidos no actualizados debido a cachés ====
Cuando se envía una petición HTTP es posible que, si la caché del lado servidor no está correctamente configurada, el navegador realice su propia caché local. Por lo tanto la próxima vez que realicemos una llamada a la misma URL, el navegador en lugar de hacerla **sacará el mismo resultado anterior de esa caché local**, y por lo tanto la llamada no llega al servidor jamás.
O puede que exista un proxy-caché por el medio (Telefónica/Movistar por ejemplo las ha utilizado tradicionalmente en sus servicios de acceso a Internet) que almacena peticiones anteriores y por lo tanto obtenemos únicamente una copia, sin realizar la llamada al servidor real. Eso muchas veces es lo que querremos para ahorrar procesamiento y será maravilloso, pero lo habitual en una aplicación que maneja datos es que suponga un problema, ya que **evitará que obtengamos información actualizada**.
http.setRequestHeader('If-Modified-Since', 'Wed, 1 Jan 1972 00:00:00 GMT');
Indicaremos siempre una fecha anterior a la actual como la del ejemplo y así siempre se pedirá la última versión al servidor.
* **Añadir un número aleatorio** (o cadena) a la URL de cada petición. En este caso suele funcionar muy bien el agregarle una marca temporal, es decir, añadir a continuación la fecha y hora actuales, de modo que cada una de las peticiones que se hagan va a ser diferente y por lo tanto las cachés que existan por el medio tienen que repetir siempre la petición. Por ejemplo:
http.open("GET", "misdatos.aspx?pasacache=" + new Date().getTime(), true);
Se le añade un parámetro que lleva como valor la fecha y hora en formato numérico (es decir, un número muy largo y que cambia varias veces cada milisegundo), por lo que es muy difícil que se den dos peticiones idénticas incluso a través de un proxy-caché.
Además ese parámetro extra de nombre inventado que nosotros le añadimos no debería afectar en absoluto a la llamada, puesto que no está siendo tenido en cuenta por la aplicación. Esta segunda técnica es la más fiable, aunque un poco más tediosa de implementar.
===== Devolución de información JSON =====
En el ejemplo anterior hemos hecho que la página del servidor devuelva ciertos valores, que en este caso tenían formato XML, pero que podrían tener perfectamente otra configuración distinta, como por ejemplo simples valores separados por comas. Si bien esto puede ser suficiente en los casos más sencillos, en otras ocasiones necesitaremos **manejar estructuras de datos más complejas**.
Dado que HTTP es un protocolo basado en texto, el recurso al que llamemos en el servidor debe devolver siempre texto (o sea, no puede ser una imagen o un archivo binario, que para transferirse se codifican de una forma especial -Base64- para convertirlos en texto).
Este texto devuelto puede tener cualquier formato: **texto plano, XML, código JavaScript o incluso HTML**. En este último caso podemos obtener desde el servidor un contenido HTML completo que se debe escribir en una zona de la página (por ejemplo dentro de un ''%%
José Manuel
Alarcón Aguín
campusMVP
986 165 802
40
Ahora consideremos la misma representación en JSON:
{
"nombre" : "José Manuel",
"apellidos" : "Alarcón Aguín",
"empresa" : "campusMVP",
"telefono" : "986 165 802",
"edad" : 40
}
¿Te suena de algo?: Efectivamente. No es más que un objeto JavaScript normal y corriente. Crearlo es muy fácil pues es sintaxis JavaScript normal. En [[http://www.json.org/|www.json.org]] es posible encontrar una explicación completa de esta notación, que ya hemos estudiado anteriormente.
Como se puede comprobar ocupa menos que el XML equivalente, es igual o incluso más fácil de leer que éste, y permite usar datos nativos y no sólo cadenas para representar los valores. En estructuras de datos más complejas se puede apreciar más todavía el ahorro de datos que implica, ya que podemos anidar objetos dentro del objeto principal.
De todos modos, lo más espectacular de JSON es lo fácil que resulta usarlo. Basta con escribir lo siguiente al recibirlo:
var cliente = eval(res);
Siendo ''res'' el nombre de la variable que contiene la cadena JSON devuelta por el servidor. Es decir, lo único que hacemos es **procesar la expresión JSON**. Al hacerlo obtenemos en la variable ''cliente'' un objeto cuyas propiedades son los datos que queremos manejar.
La función de JavaScript ''eval'' sirve para evaluar código JavaScript de manera dinámica. Si lo que evaluamos es un objeto, como en el caso del ejemplo, lo que devuelve ''eval'' es una referencia al mismo, ya en memoria. Pero puede procesar cualquier tipo de código JavaScript como si lo hubiésemos escrito en la página.
De este modo, ahora lo único que tenemos que hacer para leer cualquier dato devuelto en el nuevo objeto es escribir directamente expresiones como esta:
alert("El nombre de la empresa es " + cliente.empresa);
¡Más fácil imposible!. Nada de recorrer una jerarquía XML con el DOM o ir buscando nodo a nodo en el contenido. Se convierte en JavaScript puro y utilizable nada más llegar desde el servidor.
El problema del uso de JSON es recibir los datos de forma segura, de manera que no se nos pueda inyectar código malicioso en nuestra página, y por otro lado crear cadenas en formato JSON para enviarlas al servidor e intercambiar datos con éste desde el navegador.
Douglas Crockford creó en su día una biblioteca de JavaScript llamada [[https://github.com/douglascrockford/JSON-js|json-js]] que sirve un doble propósito:
* Convertir objetos de JavaScript a cadenas en formato JSON para poder enviar al servidor. Se consigue a través del método **stringify**.
* Procesar cadenas JSON para convertirlas a objetos JavaScript, pero en lugar de usar directamente ''eval'' (que lo procesa todo), se limita a procesar sintaxis JSON estrictamente, con datos puros y duros. Además permite convertir los datos antes de crear el objeto. El método para ello se llama ''parse''.
La biblioteca crea un objeto global llamado JSON que tiene estos dos métodos, a los que podemos llamar así:
var client = JSON.parse(res);
var sJson = JSON.stringify(miObjeto);
Aunque tienen algunos parámetros avanzados más.
La biblioteca es tan útil y ha sido tan exitosa que el ECMA decidió incluir el objeto JSON con estas dos funciones como parte integral del estándar en la versión 5 de la especificación del lenguaje (¡guau!, esto es tener éxito, ¿verdad?), por lo que **todos los navegadores actuales disponen de esa funcionalidad incluida de serie**.
Está soportada desde la versión 8 en Internet Explorer, la 3.6 de Firefox, la 19 de Chrome, la 5.1 de Safari y la 12 de Opera. Todos los navegadores móviles la soportan también.
De todos modos, si prevés que vas a tener que dar soporte a navegadores muy antiguos puedes descargarte desde el enlace anterior la biblioteca **json2.js** y utilizarla para, por el mero hecho de incluirla, dar soporte a navegadores que no incluyan el objeto JSON de serie.
===== JSONP: Accediendo a datos en otros dominios =====
La medida de seguridad y control más generalizada en los navegadores es la imposibilidad de acceso a recursos de otros dominios. Ya hemos presentado este problema en una lección anterior.
En cualquier navegador **cada dominio es una zona aislada** de las demás. Este aislamiento impide que, por defecto, se puedan transferir cookies entre dominios, que fallen los scripts que tratan de afectar a marcos con páginas que están en dominios diferentes y, por supuesto, tampoco permite hacer peticiones desde código JavaScript a servidores que están en dominios diferentes a la actual. Y aun así existen cantidad de vulnerabilidades (//Cross Site Scripting//, //Cross Site Request Forgery//, //Cross Zone Scripting//, etc...) relacionadas con robo de información y ataques distribuidos que están basadas en explotar código de JavaScript en aplicaciones mal construidas.