AJAX: Asynchronous JavaScript And XML

El término AJAX es el acrónimo de Asynchronous JavaScript And XML, Javascript Asíncrono y XML. Se trata de una técnica para usar el protocolo HTTP mediante Javascript. Aunque en el nombre pone XML, puede usarse simplemente para solicitar páginas al servidor con Javascript, documentos que al fin y al cabo son contenidos de texto. Aunque también pueden manejarse documentos XML e insertarlos en el DOM del navegador para su posterior acceso.

Lo de asíncrono se debe a la característica para descargar contenidos desde el servidor en segundo plano. Así mientras se van recuperando esos datos, la página puede seguir su curso de ejecución. AJAX no es un lenguaje de programación, sino una técnica basada en el objeto XMLHttpRequest y combinada con JavaScript, HTML y CSS. Hay una especificación oficial en w3.org, aunque también hay otros sitios donde aprender más sobre esto como librosweb.es o w3schools.com.

Realmente el componente que permitía este uso es bastante antiguo, pues los elementos iframe de Internet Explorer (1996) ya permitían la descarga de documentos en un marco separado y de forma asíncrona. Más adelante (1999) Microsoft creó el objeto ActiveX llamado XMLHTTP, primer precursor del actual XMLHttpRequest y que luego sería también incluido en otros navegadores. En el 2006 el World Wide Web Consortium (W3C) emite la primera especificación XMLHttpRequest, definiéndola como una API Application Programming Interface (Interfaz de programación de aplicaciones): Es el conjunto de funciones, procedimientos o métodos que crean una capa de abstracción entre dos aplicaciones interconectándolas. que provee funcionalidad de programación en el cliente mediante transferencia de datos entre cliente y servidor.

Desplegables encadenados

Son muchas las utilidades de AJAX. Pero también hay que saber para que sirve, pues un uso inadecuado nos restará eficiencia tanto en el navegador como en el servidor. Hay un tipo de problema ilustrativo con el que se evidencia la utilidad de esta técnica. Se trata de no traer con la página un volumen de datos grande, sino irlos solicitando a medida que el usuario los requiera. Un ejemplo es el de los desplegables encadenados.

Supongamos que tenemos un formulario para rellenar la provincia y el municipio. En España hay 52 provincias y alrededor de 8100 municipios. Podríamos tener dos desplegables como esta imagen en nuestro formulario:

provincias y municipios

Cuando el usuario elige la provincia se cargará el segundo desplegable con la lista de municipios de esa provincia. Los datos de las 52 provincias se encuentran en un archivo de texto que ocupa menos de 1KB y es así:

1   Álava
2   Albacete
3   Alicante/Alacant
4   Almería
5   Ávila
...

Los 8110 municipios están también en un archivo de texto que ocupa unos 140 KB y con este formato:

1  Alegría-Dulantzi
1  Amurrio
1  Añana
1  Aramaio
1  Armiñón
...

Están ordenados por el número de la provincia y luego alfabéticamente el nombre del municipio. Estos cinco primeros son los de la provincia 1. Hay que hacer un formulario que reciba y valide los campos y devuelva el resultado. Se pueden enviar los 140KB de datos con todos los municipios al usuario y manejar en el navegador con Javascript todo el proceso. Pero es una carga desaconsejable pues el usuario sólo va a necesitar los municipios de su provincia. Esto lo podemos conseguir de estas dos maneras:

  1. Usando un módulo PHP aparte que seleccionará los municipios de una provincia determinada, reenviando la página de nuevo: mun.php
  2. Usando AJAX para este cometido: mun-ajax.php

En los siguientes apartados veremos los detalles.

Desplegables encadenados con PHP

Tenemos una página PHP que contiene el formulario y al mismo tiempo lo recibe para devolver el resultado. Puede ver como funciona básicamente en formularios reentrantes. Se trata de recibir los campos prov y mun de los desplegables para la provincia y el municipio. El primero recogerá el código numérico de la provincia y el segundo el nombre del municipio, es decir, una cadena de texto. El código de la página PHP es el siguiente:

include_once ("[RUTA...]/selecciona_municipos.php");
$lista_municipios = "";
$codigo_provincia = 0;
$municipio = "-";
$enviado = false;
$campos_recibidos = 0;
$presentar_form = true;
foreach($_POST as $campo=>$valor){
    switch ($campo) {
        case "prov":
            $codigo_provincia = intval(htmlspecialchars($valor, ENT_QUOTES));
            if (($codigo_provincia > 0)&&
                ($codigo_provincia < 53)) $campos_recibidos++;
            break;                
        case "mun":
            $municipio = htmlspecialchars($valor, ENT_QUOTES);
            if (($municipio != "")&&
                ($municipio != "-")) $campos_recibidos++;
            break;
        case "envio":
            $enviado = true;
            break;
    }
}
echo '<?xml version="1.0" encoding="UTF-8"?>';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
<head>
    <title>Provincias y municipios con PHP</title>
    ...
</head>
<body>
    <?php 
    if ($enviado && ($campos_recibidos == 2)){
        $presentar_form = false; 
        $nom_prov = nombre_provincia($codigo_provincia);
    ?>
       <p style='color: green'>Hemos recibido sus datos siguientes:</p>
       <ul>
            <li>Provincia: <?php echo "[".$codigo_provincia."] ".$nom_prov; 
                ?></li>
            <li>Municipio: <?php echo $municipio; ?></li>
        </ul>
    <?php
    } else if ($enviado && ($campos_recibidos < 2)) { ?>
        <p style='color: red'>Faltó algún dato.</p>           
    <?php 
    }
    if ($presentar_form){
        $select_prov = despliega_provincias($codigo_provincia);
        $select_mun = despliega_municipios($codigo_provincia, $municipio);        
    ?>
        <form method="post" action="mun.php">
            Provincia:
            <select name="prov" onchange="this.form.submit()">
                <?php echo $select_prov; ?>
            </select>
            Municipio:
            <select name="mun">
                <?php  echo $select_mun; ?>
            </select>
            <br />
            <input type="submit" name="envio" value="enviar" />
        </form>
    <?php } ?>
    ...

Detectamos que se ha enviado el formulario con el botón de tipo submit observando el nombre "envio" de ese botón. En ese caso el formulario resultará "enviado" y si tiene dos campos podemos presentar el resultado. Si está "enviado" y tiene menos de 2 campos avisaremos de que falta algún dato. En este y en el caso de que el formulario no se encuentre en situación de "enviado" presentaremos ese formulario. Y esto sucederá la primera vez que lo abrimos y cuando se ejecute el evento onchange del desplegable de la provincia.

Resaltado en colores tenemos las funciones que se ejecutan en el módulo PHP selecciona-municipios.php para la selección de los datos. La [RUTA...] es la que corresponda a la carpeta no pública donde pongamos los script PHP. Puede consultar el código de este módulo, pues no viene al caso entrar de lleno en el mismo pues nuestro objetivo es ver el comportamiento con este caso de PHP y luego con AJAX. En resumen estas funciones hacen lo siguiente:

  • despliega_municipios($cod_prov, $municipio): Dado un código numérico de la provincia, seleccionará todos sus municipios devolviéndolos en un literal HTML con elementos <option>. Si hay una cadena de texto para el argumento del municipio, a ese elemento se le incluirá el atributo selected="selected" como seleccionado.
  • despliega_provincias($cod_prov): Literal HTML de elementos <option> en este caso para los provincias.
  • nombre_provincia($cod_prov): Dado un código numérico de provincia devolverá el nombre de la misma.

Este módulo sólo esta diseñado para servir de ejemplo, pues en un caso real sería conveniente usar un método más eficiente como bases de datos por ejemplo. Pero nos servirá para observar el comportamiento cuando se modifique el valor del primer desplegable observando que se remite el formulario y se actualiza completamente la página. Este comportamiento apenas resulta molesto debido al poco contenido del ejemplo. Pero si esa página estuviese más cargada veríamos que ya no resulta tan cómodo.

Desplegables encadenados con AJAX

El PHP que envía y recibe el formulario es el mismo que el anterior, pero ahora no usamos el módulo selecciona-municipios.php para buscar municipios. El código que difiere del anterior es el que antes resaltábamos en colores y que ahora es sustituido por el siguiente, siendo el resto exactamente igual:

$lista_provincias = array("-","Álava","Albacete","Alicante/Alacant","Almería","Ávila",
"Badajoz","Balears (Illes)","Barcelona","Burgos","Cáceres","Cádiz","Castellón/Castellá",
"Ciudad Real","Córdoba","Coruña (A)","Cuenca","Girona","Granada","Guadalajara","Guipúzcoa",
"Huelva","Huesca","Jaén","León","Lleida","Rioja (La)","Lugo","Madrid","Málaga","Murcia",
"Navarra","Ourense","Asturias","Palencia","Palmas (Las)","Pontevedra","Salamanca",
"Santa Cruz de Tenerife","Cantabria","Segovia","Sevilla","Soria","Tarragona","Teruel",
"Toledo","Valencia/València","Valladolid","Vizcaya","Zamora","Zaragoza","Ceuta","Melilla");
...    
$nom_prov = $lista_provincias[$codigo_provincia];
...
$select_prov = "";
foreach($lista_provincias as $i => $prov){
    $select_prov .= '<option value="'.$i.'"';
    if ($i == $codigo_provincia){
        $select_prov .= ' selected="selected"';
    }
    $select_prov .= '>'.$prov.'</option>';
}        
$select_mun = '<option value="-">-</option>';

Hay otro detalle que también difiere. Antes teníamos el desplegable de provincias con onchange="this.form.submit()" para remitir el formulario. Ahora ponemos onchange="buscaMun(this)" que ejecutará una función JavaScript sin salir del navegador, código que vemos a continuación. Pasamos todas las provincias en un array, lo que nos servirá para obtener el nombre de la provincia consultándolo así como para llenar el desplegable de provincias. En cambio no rellenamos el de municipios. De esto se encargará el AJAX, cuyo código JavaScript lo enviamos en un elemento <script> en la cabecera del documento:

var request= null;

try {
    if (window.XMLHttpRequest){
        request = new XMLHttpRequest();
    } else {
        request = new ActiveXObject("Microsoft.XMLHTTP"); 
    }
} catch (e) {
    alert("No se pudo crear un objeto XML. " + e.message);
}  

function peticiona(estaUrl){
    if (request){
        request.open("GET", estaUrl, true);
        request.onreadystatechange = recogeResultados;
        request.setRequestHeader("Cache-Control", 
            "no-cache, must-revalidate");
        request.setRequestHeader("If-Modified-Since", 
            "Sat, 1 Jan 2005 00:00:00 GMT");                
        request.send();
    } else {
        alert("No hay objeto XMLHttpRequest.");
    }
}

function recogeResultados(){
    if ((request!=null)&&(request.readyState == 4)&&
        (request.status == 200)){
        var arr = request.responseText.split("\n");
        var mun = document.getElementById("mun");
        mun.innerHTML = "";
        for (var i=0; i<arr.length; i++){
            if (arr[i] != ""){
                var opt = document.createElement("option");
                opt.setAttribute("value", arr[i]);
                var optText = document.createTextNode(arr[i]);
                opt.appendChild(optText);
                mun.appendChild(opt);
            }
        }
    }
}

function buscaMun(prov){
    var numProv = prov.value;
    if (numProv == "-") {
        document.getElementById("mun").innerHTML = "";
    } else {
        peticiona("dat-mun-ajax/mun" + numProv + ".txt");
    }
}

Cuando se carga la página también se declara el objeto que hemos denominado request con un XMLHttpRequest. Las versiones de Internet Explorer más antiguas no soportan este objeto y le damos una alternativa con un ActiveXObject. La función buscaMun(prov) se lanza cuando seleccionamos una provincia en ese desplegable, vaciando el de municipios y haciendo una petición a la ruta "dat-mun-ajax/mun" + numProv + ".txt". Como numProv es el código numérico de la provincia, por ejemplo con el 1 tendríamos la ruta "dat-mun-ajax/mun1.txt". Entonces disponemos en el servidor de 52 archivos de texto cada uno con las municipios de una provincia. Por ejemplo en el archivo mun1.txt están los municipios de la provincia 1. La función peticiona(estaUrl) se compone de:

  1. request.open("GET", estaUrl, true): abre una petición de tipo GET con la URL pasada. Al ser relativa se le antepone la base de la página. Este método del objeto XMLHttpRequest se declara como open(método, url, asíncrono). Si ponemos asíncrono a false entonces la página se detiene hasta que el servidor le responda. Si la respuesta es rápida no habrá apenas diferencias, pero si el servidor se demora por alguna razón, la página también permanecerá en espera. En el fondo no debe usarse de esta forma síncrona pues es contraria a la esencia de AJAX.
  2. request.onreadystatechange = recogeResultados: Se trata de un evento que se ejecutará cuando el XMLHttpRequest esté listo al haber recibido los datos o bien se haya encontrado con un error. Apunta a una función declarada aparte denominada recogeResultados().
  3. A continuación hay dos líneas para evitar que el navegador almacene en caché el resultado. Esto es importante pues realmente el objeto XMLHttpRequest hace uso del protocolo HTTP y si encuentra una versión de los datos en caché, ésta es la que será servida. Pero esto que podría valer en otro contexto, no nos sirve para nuestro ejemplo pues necesitamos datos nuevos en cada petición.
  4. La petición finaliza haciendo request.send(); con lo que será enviada al servidor.

El evento se maneja en la función recogeResultados(). El objeto XMLHttpRequest puede devolver estos estados:

  • 0: El objeto ha sido construido
  • 1: El objeto ha sido abierto con open()
  • 2: El objeto ha recibido las cabeceras
  • 3: El objeto está recibiendo el cuerpo
  • 4: El objeto ya ha recibido todo el documento

Por lo tanto hemos de controlar cuando recibamos el estado 4 para proceder a manejar los datos recibidos. Además también hay que revisar que el código de estado HTTP sea 200, que significa que el servidor encontró y sirvió sin problemas el documento solicitado. En ese caso tenemos un archivo de texto plano con los nombres de los municipios de la provincia, algo como por ejemplo:

Alegría-Dulantzi
Amurrio
Añana
Aramaio
Armiñón    
...

El objeto XMLHttpRequest tiene dos posibilidades para obtener los resultados. Una es con request.responseText puesto que el protocolo HTTP siempre obtiene el documento en texto plano. Pero si se recibe un XML también podemos cargar ese contenido de texto con request.responseXML en el DOM del navegador. Para nuestro caso nos interesa sólo el contenido de texto, pues vamos a aplicar el método split para dividirlo en líneas y meterlo en un array. Luego procedemos a crear dinámicamente los elementos <option> para cargar el desplegable de municipios.

Esta ejecución se produce en segundo plano, es decir, la página sólo es actualizada en este elemento. El ejemplo es simple pero ilustrativo. Y aunque hemos puesto los municipios en archivos de texto separados en el servidor, bien podrían formar parte de una única tabla de una base de datos y que fuese ésta la encargada de hacer la selección. O con PHP servir también el literal HTML de los elementos <option> tal como hicimos en el primer ejemplo. En definitiva, la potencialidad de AJAX es más que evidente.