Una de las operaciones más habituales en las aplicaciones AJAX es la de obtener el contenido de un archivo o recurso del servidor. Por tanto, se va a construir un objeto que permita realizar la carga de datos del servidor simplemente indicando el recurso solicitado y la función encargada de procesar la respuesta:
var cargador = new net.CargadorContenidos("pagina.html", procesaRespuesta);
La lógica común de AJAX se encapsula en un objeto de forma que sea fácilmente reutilizable. Aplicando los conceptos de objetos de JavaScript, funciones constructoras y el uso de prototype, es posible realizar de forma sencilla el objeto cargador de contenidos.
El siguiente código ha sido adaptado del excelente libro "Ajax in Action", escrito por Dave Crane, Eric Pascarello y Darren James y publicado por la editorial Manning.
var net = new Object(); net.READY_STATE_UNINITIALIZED=0; net.READY_STATE_LOADING=1; net.READY_STATE_LOADED=2; net.READY_STATE_INTERACTIVE=3; net.READY_STATE_COMPLETE=4; // Constructor net.CargadorContenidos = function(url, funcion, funcionError) { this.url = url; this.req = null; this.onload = funcion; this.onerror = (funcionError) ? funcionError : this.defaultError; this.cargaContenidoXML(url); } net.CargadorContenidos.prototype = { cargaContenidoXML: function(url) { if(window.XMLHttpRequest) { this.req = new XMLHttpRequest(); } else if(window.ActiveXObject) { this.req = new ActiveXObject("Microsoft.XMLHTTP"); } if(this.req) { try { var loader = this; this.req.onreadystatechange = function() { loader.onReadyState.call(loader); } this.req.open('GET', url, true); this.req.send(null); } catch(err) { this.onerror.call(this); } } }, onReadyState: function() { var req = this.req; var ready = req.readyState; if(ready == net.READY_STATE_COMPLETE) { var httpStatus = req.status; if(httpStatus == 200 || httpStatus == 0) { this.onload.call(this); } else { this.onerror.call(this); } } }, defaultError: function() { alert("Se ha producido un error al obtener los datos" + "\n\nreadyState:" + this.req.readyState + "\nstatus: " + this.req.status + "\nheaders: " + this.req.getAllResponseHeaders()); } }
Una vez definido el objeto net
con su método CargadorContenidos()
, ya es posible utilizarlo en las funciones que se encargan de mostrar el contenido del archivo del servidor:
function muestraContenido() { alert(this.req.responseText); } function cargaContenidos() { var cargador = new net.CargadorContenidos("http://localhost/holamundo.txt", muestraContenido); } window.onload = cargaContenidos;
En el ejemplo anterior, la aplicación muestra un mensaje con los contenidos de la URL indicada:
Figura 7.1. Mensaje mostrado cuando el resultado es exitoso
Por otra parte, si la URL que se quiere cargar no es válida o el servidor no responde, la aplicación muestra el siguiente mensaje de error:
Figura 7.2. Mensaje mostrado cuando el resultado es erróneo
El código del cargador de contenidos hace un uso intensivo de objetos, JSON, funciones anónimas y uso del objeto this
. Seguidamente, se detalla el funcionamiento de cada una de sus partes.
El primer elemento importante del código fuente es la definición del objeto net
.
var net = new Object();
Se trata de una variable global que encapsula todas las propiedades y métodos relativos a las operaciones relacionadas con las comunicaciones por red. De cierto modo, esta variable global simula el funcionamiento de los namespaces ya que evita la colisión entre nombres de propiedades y métodos diferentes.
Después de definir las constantes empleadas por el objeto XMLHttpRequest
, se define el constructor del objeto CargadorContenidos
:
net.CargadorContenidos = function(url, funcion, funcionError) { this.url = url; this.req = null; this.onload = funcion; this.onerror = (funcionError) ? funcionError : this.defaultError; this.cargaContenidoXML(url); }
Aunque el constructor define tres parámetros diferentes, en realidad solamente los dos primeros son obligatorios. De esta forma, se inicializa el valor de algunas variables del objeto, se comprueba si se ha definido la función que se emplea en caso de error (si no se ha definido, se emplea una función genérica definida más adelante) y se invoca el método responsable de cargar el recurso solicitado (cargaContenidoXML
).
net.CargadorContenidos.prototype = { cargaContenidoXML:function(url) { ... }, onReadyState:function() { ... }, defaultError:function() { ... } }
Los métodos empleados por el objeto net.cargaContenidos
se definen mediante su prototipo. En este caso, se definen tres métodos diferentes: cargaContenidoXML()
para cargar recursos de servidor, onReadyState()
que es la función que se invoca cuando se recibe la respuesta del servidor y defaultError()
que es la función que se emplea cuando no se ha definido de forma explícita una función responsable de manejar los posibles errores que se produzcan en la petición HTTP.
La función defaultError()
muestra un mensaje de aviso del error producido y además muestra el valor de algunas de las propiedades de la petición HTTP:
defaultError:function() { alert("Se ha producido un error al obtener los datos" + "\n\nreadyState:" + this.req.readyState + "\nstatus: " + this.req.status + "\nheaders: " + this.req.getAllResponseHeaders()); }
En este caso, el objeto this
se resuelve al objeto net.cargaContenidos
, ya que es el objeto que contiene la función anónima que se está ejecutando.
Por otra parte, la función onReadyState
es la encargada de gestionar la respuesta del servidor:
onReadyState: function() { var req = this.req; var ready = req.readyState; if(ready == net.READY_STATE_COMPLETE) { var httpStatus = req.status; if(httpStatus == 200 || httpStatus == 0) { this.onload.call(this); } else { this.onerror.call(this); } } }
Tras comprobar que la respuesta del servidor está disponible y es correcta, se realiza la llamada a la función que realmente procesa la respuesta del servidor de acuerdo a las necesidades de la aplicación.
this.onload.call(this);
El objeto this
se resuelve como net.CargadorContenidos
, ya que es el objeto que contiene la función que se está ejecutando. Por tanto, this.onload
es la referencia a la función que se ha definido como responsable de procesar la respuesta del servidor (se trata de una referencia a una función externa).
Normalmente, la función externa encargada de procesar la respuesta del servidor, requerirá acceder al objeto XMLHttpRequest
que almacena la petición realizada al servidor. En otro caso, la función externa no será capaz de acceder al contenido devuelto por el servidor.
Como ya se vio en los capítulos anteriores, el método call()
es uno de los métodos definidos para el objeto Function()
, y por tanto disponible para todas las funciones de JavaScript. Empleando el método call()
es posible obligar a una función a ejecutarse sobre un objeto concreto. En otras palabras, empleando el método call()
sobre una función, es posible que dentro de esa función el objeto this
se resuelva como el objeto pasado como parámetro en el método call()
.
Así, la instrucción this.onload.call(this);
se interpreta de la siguiente forma:
this
que se pasa como parámetro de call()
se resuelve como el objeto net.CargadorContenidos
.this.onload
almacena una referencia a la función externa que se va a emplear para procesar la respuesta.this.onload.call()
ejecuta la función cuya referencia se almacena en this.onload
.this.onload.call(this);
permite ejecutar la función externa con el objeto net.CargadorContenidos
accesible en el interior de la función mediante el objeto this
. Por último, el método cargaContenidoXML
se encarga de enviar la petición HTTP y realizar la llamada a la función que procesa la respuesta:
cargaContenidoXML:function(url) { if(window.XMLHttpRequest) { this.req = new XMLHttpRequest(); } else if(window.ActiveXObject) { this.req = new ActiveXObject("Microsoft.XMLHTTP"); } if(this.req) { try { var loader=this; this.req.onreadystatechange = function() { loader.onReadyState.call(loader); } this.req.open('GET', url, true); this.req.send(null); } catch(err) { this.onerror.call(this); } } }
En primer lugar, se obtiene una instancia del objeto XMLHttpRequest
en función del tipo de navegador. Si se ha obtenido correctamente la instancia, se ejecutan las instrucciones más importantes del método cargaContenidoXML
:
var loader = this; this.req.onreadystatechange = function() { loader.onReadyState.call(loader); } this.req.open('GET', url, true); this.req.send(null);
A continuación, se almacena la instancia del objeto actual (this
) en la nueva variable loader
. Una vez almacenada la instancia del objeto net.cargadorContenidos
, se define la función encargada de procesar la respuesta del servidor. En la siguiente función anónima:
this.req.onreadystatechange = function() { ... }
En el interior de esa función, el objeto this
no se resuelve en el objeto net.CargadorContenidos
, por lo que no se puede emplear la siguiente instrucción:
this.req.onreadystatechange = function() { this.onReadyState.call(loader); }
Sin embargo, desde el interior de esa función anónima si es posible acceder a las variables definidas en la función exterior que la engloba. Así, desde el interior de la función anónima sí que es posible acceder a la instancia del objeto net.CargadorContenidos
que se almacenó anteriormente.
En el código anterior, no es obligatorio emplear la llamada al método call()
. Se podría haber definido de la siguiente forma:
var loader=this; this.req.onreadystatechange = function() { // loader.onReadyState.call(loader); loader.onReadyState(); }
En el interior de la función onReadyState
, el objeto this
se resuelve como net.ContentLoader
, ya que se trata de un método definido en el prototipo del propio objeto.
Ejercicio 12
La página HTML proporcionada incluye una zona llamada ticker en la que se deben mostrar noticias generadas por el servidor. Añadir el código JavaScript necesario para:
XMLHttpRequest
para hacer las diferentes peticiones.Descargar ZIP con la página HTML y el script generaContenidos.php