Introducción a AJAX

7.5. Interacción con el servidor

7.5.1. Envío de parámetros con la petición HTTP

Hasta ahora, el objeto XMLHttpRequest se ha empleado para realizar peticiones HTTP sencillas. Sin embargo, las posibilidades que ofrece el objeto XMLHttpRequest son muy superiores, ya que también permite el envío de parámetros junto con la petición HTTP.

El objeto XMLHttpRequest puede enviar parámetros tanto con el método GET como con el método POST de HTTP. En ambos casos, los parámetros se envían como una serie de pares clave/valor concatenados por símbolos &. El siguiente ejemplo muestra una URL que envía parámetros al servidor mediante el método GET:

http://localhost/aplicacion?parametro1=valor1&parametro2=valor2&parametro3=valor3

La principal diferencia entre ambos métodos es que mediante el método POST los parámetros se envían en el cuerpo de la petición y mediante el método GET los parámetros se concatenan a la URL accedida. El método GET se utiliza cuando se accede a un recurso que depende de la información proporcionada por el usuario. El método POST se utiliza en operaciones que crean, borran o actualizan información.

Técnicamente, el método GET tiene un límite en la cantidad de datos que se pueden enviar. Si se intentan enviar más de 512 bytes mediante el método GET, el servidor devuelve un error con código 414 y mensaje Request-URI Too Long "La URI de la petición es demasiado larga").

Cuando se utiliza un elemento <form> de HTML, al pulsar sobre el botón de envío del formulario, se crea automáticamente la cadena de texto que contiene todos los parámetros que se envían al servidor. Sin embargo, el objeto XMLHttpRequest no dispone de esa posibilidad y la cadena que contiene los parámetros se debe construir manualmente.

A continuación se incluye un ejemplo del funcionamiento del envío de parámetros al servidor. Se trata de un formulario con tres campos de texto que se validan en el servidor mediante AJAX. El código HTML también incluye un elemento <div> vacío que se utiliza para mostrar la respuesta del servidor:

<form>
  <label for="fecha_nacimiento">Fecha de nacimiento:</label>
  <input type="text" id="fecha_nacimiento" name="fecha_nacimiento" /><br/>

  <label for="codigo_postal">Codigo postal:</label>
  <input type="text" id="codigo_postal" name="codigo_postal" /><br/>

  <label for="telefono">Telefono:</label>
  <input type="text" id="telefono" name="telefono" /><br/>

  <input type="button" value="Validar datos" />
</form>

<div id="respuesta"></div>

El código anterior produce la siguiente página:

Formulario de ejemplo

Figura 7.3 Formulario de ejemplo

El código JavaScript necesario para realizar la validación de los datos en el servidor se muestra a continuación:

var READY_STATE_COMPLETE=4;
var peticion_http = null;

function inicializa_xhr() {
  if(window.XMLHttpRequest) {
    return new XMLHttpRequest();
  }
  else if(window.ActiveXObject) {
    return new ActiveXObject("Microsoft.XMLHTTP");
  }
}

function crea_query_string() {
  var fecha = document.getElementById("fecha_nacimiento");
  var cp = document.getElementById("codigo_postal");
  var telefono = document.getElementById("telefono");

  return "fecha_nacimiento=" + encodeURIComponent(fecha.value) +
         "&codigo_postal=" + encodeURIComponent(cp.value) +
         "&telefono=" + encodeURIComponent(telefono.value) +
         "&nocache=" + Math.random();
}

function valida() {
  peticion_http = inicializa_xhr();
  if(peticion_http) {
    peticion_http.onreadystatechange = procesaRespuesta;
    peticion_http.open("POST", "http://localhost/validaDatos.php", true);

    peticion_http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    var query_string = crea_query_string();
    peticion_http.send(query_string);
  }
}

function procesaRespuesta() {
  if(peticion_http.readyState == READY_STATE_COMPLETE) {
    if(peticion_http.status == 200) {
      document.getElementById("respuesta").innerHTML = peticion_http.responseText;
    }
  }
}

La clave del ejemplo anterior se encuentra en estas dos líneas de código:

peticion_http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
peticion_http.send(query_string);

En primer lugar, si no se establece la cabecera Content-Type correcta, el servidor descarta todos los datos enviados mediante el método POST. De esta forma, al programa que se ejecuta en el servidor no le llega ningún parámetro. Así, para enviar parámetros mediante el método POST, es obligatorio incluir la cabecera Content-Type mediante la siguiente instrucción:

peticion_http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

Por otra parte, el método send() es el que se encarga de enviar los parámetros al servidor. En todos los ejemplos anteriores se utilizaba la instrucción send(null) para indicar que no se envían parámetros al servidor. Sin embargo, en este caso la petición si que va a enviar los parámetros.

Como ya se ha comentado, los parámetros se envían en forma de cadena de texto con las variables y sus valores concatenados mediante el símbolo & (esta cadena normalmente se conoce como "query string"). La cadena con los parámetros se construye manualmente, para lo cual se utiliza la función crea_query_string():

function crea_query_string() {
  var fecha = document.getElementById("fecha_nacimiento");
  var cp = document.getElementById("codigo_postal");
  var telefono = document.getElementById("telefono");

  return "fecha_nacimiento=" + encodeURIComponent(fecha.value) +
         "&codigo_postal=" + encodeURIComponent(cp.value) +
         "&telefono=" + encodeURIComponent(telefono.value) +
         "&nocache=" + Math.random();
}

La función anterior obtiene el valor de todos los campos del formulario y los concatena junto con el nombre de cada parámetro para formar la cadena de texto que se envía al servidor. El uso de la función encodeURIComponent() es imprescindible para evitar problemas con algunos caracteres especiales.

La función encodeURIComponent() reemplaza todos los caracteres que no se pueden utilizar de forma directa en las URL por su representación hexadecimal. Las letras, números y los caracteres - _ . ! ~ * ' ( ) no se modifican, pero todos los demás caracteres se sustituyen por su equivalente hexadecimal.

Las sustituciones más conocidas son las de los espacios en blanco por %20, y la del símbolo & por %26. Sin embargo, como se muestra en el siguiente ejemplo, también se sustituyen todos los acentos y cualquier otro carácter que no se puede incluir directamente en una URL:

var cadena = "cadena de texto";
var cadena_segura = encodeURIComponent(cadena);
// cadena_segura = "cadena%20de%20texto";

var cadena = "otra cadena & caracteres problemáticos / : =";
var cadena_segura = encodeURIComponent(cadena);
// cadena_segura = "otra%20cadena%20%26%20caracteres%20problem%C3%A1ticos%20%2F%20%3A%20%3D";

JavaScript incluye una función contraria llamada decodeURIComponent() y que realiza la transformación inversa. Además, también existen las funciones encodeURI() y decodeURI() que codifican/decodifican una URL completa. La principal diferencia entre encodeURIComponent() y encodeURI() es que esta última no codifica los caracteres ; / ? : @ & = + $ , #:

var cadena = "http://www.ejemplo.com/ruta1/index.php?parametro=valor con ñ y &";
var cadena_segura = encodeURIComponent(cadena);
// cadena_segura = "http%3A%2F%2Fwww.ejemplo.com%2Fruta1%2Findex.php%3Fparametro%3Dvalor%20con%20%C3%B1%20y%20%26";

var cadena_segura = encodeURI(cadena); // cadena_segura = "http://www.ejemplo.com/ruta1/index.php?parametro=valor%20con%20%C3%B1%20y%20";

Por último, la función crea_query_string() añade al final de la cadena un parámetro llamado nocache y que contiene un número aleatorio (creado mediante el método Math.random()). Añadir un parámetro aleatorio adicional a las peticiones GET y POST es una de las estrategias más utilizadas para evitar problemas con la caché de los navegadores. Como cada petición varía al menos en el valor de uno de los parámetros, el navegador está obligado siempre a realizar la petición directamente al servidor y no utilizar su cache. A continuación se muestra un ejemplo de la query string creada por la función definida:

Query String creada para el formulario de ejemplo

Figura 7.4 Query String creada para el formulario de ejemplo

En este ejemplo sencillo, el servidor simplemente devuelve el resultado de una supuesta validación de los datos enviados mediante AJAX:

Mostrando el resultado devuelto por el servidor

Figura 7.5 Mostrando el resultado devuelto por el servidor

En las aplicaciones reales, las validaciones de datos mediante AJAX sólo se utilizan en el caso de validaciones complejas que no se pueden realizar mediante el uso de código JavaScript básico. En general, las validaciones complejas requieren el uso de bases de datos: comprobar que un nombre de usuario no esté previamente registrado, comprobar que la localidad se corresponde con el código postal indicado, etc.

Ejercicio 13

Un ejemplo de validación compleja es la que consiste en comprobar si un nombre de usuario escogido está libre o ya lo utiliza otro usuario. Como es una validación que requiere el uso de una base de datos muy grande, no se puede realizar en el navegador del cliente. Utilizando las técnicas mostradas anteriormente y la página web que se proporciona:

  1. Crear un script que compruebe con AJAX y la ayuda del servidor si el nombre escogido por el usuario está libre o no.
  2. El script del servidor se llama compruebaDisponibilidad.php y el parámetro que contiene el nombre se llama login.
  3. La respuesta del servidor es "si" o "no", en función de si el nombre de usuario está libre y se puede utilizar o ya ha sido ocupado por otro usuario.
  4. A partir de la respuesta del servidor, mostrar un mensaje al usuario indicando el resultado de la comprobación.

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

Ver solución

7.5.2. Refactorizando la utilidad net.CargadorContenidos

La utilidad diseñada anteriormente para la carga de contenidos y recursos almacenados en el servidor, solamente está preparada para realizar peticiones HTTP sencillas mediante GET. A continuación se refactoriza esa utilidad para que permita las peticiones POST y el envío de parámetros al servidor.

El primer cambio necesario es el de adaptar el constructor para que se puedan especificar los nuevos parámetros:

net.CargadorContenidos = function(url, funcion, funcionError, metodo, parametros, contentType) {

Se han añadido tres nuevos parámetros: el método HTTP empleado, los parámetros que se envían al servidor junto con la petición y el valor de la cabecera content-type.

A continuación, se sustituye la instrucción this.req.open('GET', url, true); por esta otra:

this.req.open(metodo, url, true);

El siguiente paso es añadir (si así se indica) la cabecera Content-Type de la petición:

if(contentType) {
  this.req.setRequestHeader("Content-Type", contentType);
}

Por último, se sustituye la instrucción this.req.send(null); por esta otra:

this.req.send(parametros);

Así, el código completo de la solución refactorizada es el siguiente:

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, metodo, parametros, contentType) {
  this.url = url;
  this.req = null;
  this.onload = funcion;
  this.onerror = (funcionError) ? funcionError : this.defaultError;
  this.cargaContenidoXML(url, metodo, parametros, contentType);
}

net.CargadorContenidos.prototype = {
  cargaContenidoXML: function(url, metodo, parametros, contentType) {
    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(metodo, url, true);
        if(contentType) {
          this.req.setRequestHeader("Content-Type", contentType);
        }
        this.req.send(parametros);
        } 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());
  }
}