Solucionando los problemas de cache al utilizar AJAX en Internet Explorer

Seguramente más de uno haya pasado por una situación similar a esta: nos encontramos testeando nuestra aplicación AJAX. Tenemos un botón (o cualquier otro elemento) cuya función es, al ser presionado, crear un objeto XMLHTTPRequest que será el encargado de recoger ciertos datos de nuestra base de datos y con ellos refrescar un determinado sector de nuestra página (sea un DIV, un SELECT u otro). Por el momento las pruebas marchan de maravilla, pero por esas cosas de la vida nos vemos en la necesidad de añadir, modificar o borrar algún registro de la BD. Hechas las modificaciones y, aún con la ventana del navegador abierta, volvemos a presionar el botón de nuestro HTML con la esperanza que aquellos cambios realizados en la base de datos se vean reflejados en nuestra página.
Si su navegador es el IE y no están aplicando ninguna de las técnicas que describiré a continuación, podrán observar que los datos mostrados en pantalla no se corresponden con los existentes actualmente en la BD, si no que se corresponden con los datos que teníamos en esta antes de realizar modificación alguna.
Estamos aquí ante un típico problema de cacheo de datos. El navegador en lugar de hacer una nueva petición al servidor en búsqueda de nuevos datos, se limita a cargar aquellos que están almacenados en su “memoria” provocando así que cualquier cambio en los datos de nuestra base de datos no se refleje en pantalla hasta que ese cache sea eliminado (cerrando y abriendo la ventana del navegador, entre otras alternativas).

En este pequeño texto intentaré describir las causas de este problema como también 3 posibles soluciones para que elijan aquella cuya implementación resulte mas sencilla y cómoda para cada situación en particular.

Un código conflictivo

A fines de poder analizar las distintas soluciones de la mejor manera posible y sin dar lugar a ambigüedades, dispondré a continuación un pequeño script cuya tarea será, al presionar el botón correspondiente, ir al soporte de datos en búsqueda de los registros que contiene y luego mostrarlos en pantalla. Solo con intenciones de simplificar los testeos, añadiré al sistema un campo input el cual tendrá como función insertar los datos que coloquemos en él en la base de datos, para así poder comprobar de manera mas sencilla los problemas descriptos anteriormente. Comenzamos definiendo la estructura de las tablas en base de datos con un juego de registros iniciales:

CREATE TABLE `cache` (
`id` int(11) NOT NULL auto_increment,
`valor` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;

INSERT INTO `cache` (`id`, `valor`) VALUES (1, 'Dato 1'),
(2, 'Dato 2');

Tendremos también dos scripts. El primero será nuestro .html que contendrá los campos de formulario y las funciones en JavaScript encargadas de la comunicación asincrónica. El segundo será el código que actúa como servidor y su función será realizar las operaciones de INSERT y SELECT en la base de datos.

ajax_cache.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<title>Cache en AJAX</title>
<script language="javascript" type="text/javascript">
function nuevoAjax()
{
        var xmlhttp=false;
        try
        {
                // Creacion del objeto AJAX para navegadores no IE
                xmlhttp=new ActiveXObject("Msxml2.XMLHTTP");
        }
        catch(e)
        {
                try
                {
                        // Creacion del objet AJAX para IE
                        xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
                }
                catch(E)
                {
                        if (!xmlhttp && typeof XMLHttpRequest!="undefined") xmlhttp=new XMLHttpRequest();
                }
        }
        return xmlhttp;
}

function mostrarDatos()
{
        ajax=nuevoAjax();
        ajax.open("GET", "ajax_cache_2.php?accion=mostrar", true);
        ajax.onreadystatechange=function()
        {
                if (ajax.readyState==4)
                {
                        document.getElementById("texto").innerHTML=ajax.responseText;
                }
        }
        ajax.send(null);
}

function guardarDato()
{
        var dato=document.getElementById("iIngreso").value;
        ajax=nuevoAjax();
        ajax.open("GET", "ajax_cache_2.php?accion=guardar&dato="+dato, true);
        ajax.onreadystatechange=function()
        {
                if (ajax.readyState==4)
                {
                        alert("Dato guardado");
                }
        }
        ajax.send(null);
}
</script>
</head>

<body>
<div id="texto"></div>
<button id="muestra" onClick="mostrarDatos()">Mostrar datos</button>
<br><br>
<input type="text" id="iIngreso"> <button id="guarda" onClick="guardarDato()">Guardar nuevo dato</button>
</body>
</html>

ajax_cache_2.php

<?php
function conectar()
{
        mysql_connect("localhost", "root", "");
        mysql_select_db("ajax");
}

function desconectar()
{
        mysql_close();
}

if($_GET["accion"]=="mostrar")
{
        conectar();
        $registros=mysql_query("SELECT valor FROM cache");
        while($fila=mysql_fetch_row($registros))
        {
                echo $fila[0]."<br>";
        }
        desconectar();
}
elseif($_GET["accion"]=="guardar")
{
        conectar();
        $dato=mysql_real_escape_string($_GET["dato"]);
        mysql_query("INSERT INTO cache (valor) VALUES ('$dato')");
        desconectar();
}
?>

Si probamos los códigos anteriores podremos observar que su funcionamiento no es como lo esperábamos en Internet Explorer. En otros navegadores, tales como Mozilla Firefox y Opera sí funcionarán correctamente ya que aplican distintas políticas en el manejo del cache.

Solución del lado del servidor

Si nuestra aplicación AJAX se conecta con un archivo PHP, ASP u otro que tengamos acceso y se nos permita modificar, la solución se reduce a una simple línea de código que colocaremos en dicho archivo antes de cualquier línea que genere salida (mediante un echo o Response.Write, por ejemplo). Esta línea de código es una cabecera que le indica al navegador que no debe utilizar su cache para mostrar los datos si no que debe, como corresponde, realizar una nueva petición y mostrar aquellos datos actualizados que obtenga como respuesta. La cabecera en PHP:

header("Cache-Control: no-store, no-cache, must-revalidate");

La cabecera en ASP:

Response.addHeader "pragma", "no-cache"
Response.CacheControl = "Private"
Response.Expires = 0);

Aplicando la solución mencionada nuestro ajax_cache_2.php nos quedará de la siguiente forma:

<?php
header("Cache-Control: no-store, no-cache, must-revalidate");

function conectar()
{
        mysql_connect("localhost", "root", "");
        mysql_select_db("ajax");
}

function desconectar()
{
        mysql_close();
}

if($_GET["accion"]=="mostrar")
{
        conectar();
        $registros=mysql_query("SELECT valor FROM cache");
        while($fila=mysql_fetch_row($registros))
        {
                echo $fila[0]."<br>";
        }
        desconectar();
}
elseif($_GET["accion"]=="guardar")
{
        conectar();
        $dato=mysql_real_escape_string($_GET["dato"]);
        mysql_query("INSERT INTO cache (valor) VALUES ('$dato')");
        desconectar();
}
?>

Soluciones del lado del cliente

En ocasiones por cuestiones técnicas, de comodidad o simplemente por prolijidad, no podremos acceder a aquellos archivos a los cuales nuestro objeto XMLHTTPRequest se conectará en busca de datos, por lo cual la solución anterior no podría ser aplicada. Para estos casos existen soluciones que se basan solo en la modificación del código JavaScript. Para comprender que es lo que hacen cada una de esas soluciones, debemos primero observar un poco sobre las causas del comportamiento del Internet Explorer y su cache. El IE cuando realiza el envío de variables vía GET a una determinada página (que llamaremos receptor.php), revisa primero que anteriormente no haya enviado una petición que contenga exactamente los mismos valores; si hubo una petición igual, cargará de su “memoria” los datos que recibió en aquella ocasión como respuesta desde el servidor. Por el contrario, si no hubo peticiones iguales, recibirá los datos actuales enviados por el servidor y los cargará en su cache para futuros usos. Por ejemplo, si hacemos una petición a receptor.php?var1=prueba&var2=ajax y el servidor nos devuelve “Hola”, cuando intentemos enviar nuevamente a receptor.php las variables ?var1=prueba&var2=ajax el IE en lugar de realizar una nueva petición al servidor, nos volverá a mostrar “Hola”, valor que ha cargado de su cache.

Viendo este comportamiento, podemos pensar que, enviando peticiones vía GET distintas, aunque solo difieran en el valor de una variable, lograremos “hacer creer” al Internet Explorer que se trata de peticiones GET diferentes y se comportará como esperamos, yendo al servidor a buscar nuevos datos. En esto se basará entonces nuestra segunda solución: al conjunto de variables que enviamos en el método open del objeto XMLHTTPRequest, le añadiremos una variable más que se generará aleatoriamente y será la encargada de generar todas peticiones GET distintas a las anteriores. Para lograr esto debemos modificar el método open de esta forma:

var aleatorio=Math.random();
ajax.open("GET", "ajax_cache_2.php?accion=mostrar&nocache="+aleatorio, true);

Nuestra función mostrarDatos() quedará de la siguiente manera:

function mostrarDatos()
{
        var aleatorio=Math.random();
        ajax=nuevoAjax();
        ajax.open("GET", "ajax_cache_2.php?accion=mostrar&aleatorio="+aleatorio, true);
        ajax.onreadystatechange=function()
        {
                if (ajax.readyState==4)
                {
                        document.getElementById("texto").innerHTML=ajax.responseText;
                }
        }
        ajax.send(null);
}

Y para finalizar, presentaré una última opción que quizá sea la menos complicada y más versátil de todas: utilizar el método POST en lugar de GET para enviar datos asincrónicamente. Por suerte, el IE no trata al método POST de igual forma que al método GET por lo que, a peticiones POST iguales, no utilizará su cache y se comportará como corresponde (¡aleluya!), al igual que el resto de los navegadores. Al utilizar POST debemos añadir el envío de un header y utilizar el método send para enviar las variables. La función mostrarDatos() quedaría:

function mostrarDatos()
{
        ajax=nuevoAjax();
        ajax.open("POST", "ajax_cache_2.php", true);
        ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        ajax.send("accion=mostrar");
        ajax.onreadystatechange=function()
        {
                if (ajax.readyState==4)
                {
                        document.getElementById("texto").innerHTML=ajax.responseText;
                }
        }
}

Y por supuesto, al recibir en ajax_cache_2.php los datos mediante POST en lugar de GET, debemos cambiar POST por GET donde corresponda, por lo que el código quedará:

<?php
function conectar()
{
        mysql_connect("localhost", "root", "");
        mysql_select_db("ajax");
}

function desconectar()
{
        mysql_close();
}

if($_POST["accion"]=="mostrar")
{
        conectar();
        $registros=mysql_query("SELECT valor FROM cache");
        while($fila=mysql_fetch_row($registros))
        {
                echo $fila[0]."<br>";
        }
        desconectar();
}
elseif($_GET["accion"]=="guardar")
{
        conectar();
        $dato=mysql_real_escape_string($_GET["dato"]);
        mysql_query("INSERT INTO cache (valor) VALUES ('$dato')");
        desconectar();
}
?>

Conclusión

Como habíamos mencionado, cualquiera de las 3 soluciones presentadas anteriormente resultan perfectamente válidas de manera independiente. Cualquiera de ellas puede ser aplicada sin necesidad de aplicar las otras 2, por lo que puedes implementar la que más te convenga para tu proyecto.

Acerca del texto

Autor: Daniel E. Pisano.
Este contenido se ha publicado bajo la licencia que figura aquí.