Introducción a AJAX

7.4. Utilidades y objetos para AJAX

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:

Mensaje mostrado cuando el resultado es exitoso

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:

Mensaje mostrado cuando el resultado es erróneo

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:

  • El objeto this que se pasa como parámetro de call() se resuelve como el objeto net.CargadorContenidos.
  • El objeto this.onload almacena una referencia a la función externa que se va a emplear para procesar la respuesta.
  • El método this.onload.call() ejecuta la función cuya referencia se almacena en this.onload.
  • La instrucción 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:

  1. De forma periódica cada cierto tiempo (por ejemplo cada segundo) se realiza una petición al servidor mediante AJAX y se muestra el contenido de la respuesta en la zona reservada para las noticias.
  2. Además del contenido enviado por el servidor, se debe mostrar la hora en la que se ha recibido la respuesta.
  3. Cuando se pulse el botón "Detener", la aplicación detiene las peticiones periódicas al servidor. Si se vuelve a pulsar sobre ese botón, se reanudan las peticiones periódicas.
  4. Añadir la lógica de los botones "Anterior" y "Siguiente", que detienen las peticiones al servidor y permiten mostrar los contenidos anteriores o posteriores al que se muestra en ese momento.
  5. Cuando se recibe una respuesta del servidor, se resalta visualmente la zona llamada ticker.
  6. Modificar la aplicación para que se reutilice continuamente el mismo objeto XMLHttpRequest para hacer las diferentes peticiones.

Descargar ZIP con la página HTML y el script generaContenidos.php

Ver solución