El problema de las versiones y la cache en Aplicaciones Web

Cuando desarrollamos aplicaciones Web que tienen gran carga de código Javascript en cliente siempre aparece un problema recurrente: Cuando se despliega una nueva versión algunos usuarios tienen problemas y errores porque el navegador cachea los archivos.

Esto causa problemas que aunque son de fácil resolución (refrescar limpiando la cache), con múltiples usuarios que no tienen que ser técnicos o saber ni que es la caché, hacen que estas soluciones no sean escalables. Hay infinidad de soluciones que pasan por añadir etiquetas <meta> en las páginas HTML:

<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
<META HTTP-EQUIV="Expires" CONTENT="-1">

o tocar los parámetros del servidor Web, pero al final siempre podemos tener el problema de que el navegador cachee un fichero con una misma URL, ya sea .html, .js, .css o .png.

Imaginemos una aplicación Web en la que múltiples ficheros .js se enlazan desde la página index.html principal. Y también que otras subpáginas HTML se invoquen desde ciertas partes del código.

esquema aplicacion web tipica

En este caso los problemas de cache pueden venirnos a diferentes niveles:

  1. Que el navegador cachee las páginas html cargadas dinámicamente.
  2. Que el navegador cachee los ficheros .js.
  3. Que el navegador cachee index.html.

Una posible solución

Por supuesto la solución de evitar el cacheo debe tener en cuenta el no penalizar la cantidad de datos que se han de descargar cada vez. Para ello vamos a proponer un sistema que solo evita el cacheo en cada versión.

1: Evitar la caché de ficheros HTML cargados dinámicamente

Esta es la solución más fácil. Como estas páginas HTML se supone que no serán muy grande, habitualmente conteniendo una sección a incorporar a la pantalla principal, basta con modificar todas las llamadas desde Javascript (por ejemplo hechas con la función load de jQuery) para que se añada una marca de tiempo en la petición.

De esta manera este código:

$(&quot;#container&quot;).load('html/view-incidence.html #container', function(response, status, xhr) {
  if (status == &quot;error&quot;) {
    console.error('Error loading incidence screen: ' + JSON.stringify(xhr));
    return;
  }
  ///...
});

Tendria que quedar de esta manera:

$(&quot;#container&quot;).load('html/view-incidence.html?t=' + new Date().getTime() + ' #container', function(response, status, xhr) {
  if (status == &quot;error&quot;) {
    console.error('Error loading incidence screen: ' + JSON.stringify(xhr));
    return;
  }
  ///...
});

Así nunca se llamara al fichero con la misma URL. Una vez el navegador llamara a html/view-incidence.html?t=142312323234 y otra a html/view-incidence.html?t=14231235644. Esta es la solución más común y sencilla para el problema más sencillo.

2: Evitar la caché de ficheros Javascript

En este caso, a menos que querramos renombrar todos los ficheros .js añadiendo su versión (práctica muy buena pero que requeriría de bastante scripting) invocando a app/controller1@1.2.3.js, proponemos una solución que se ejecuta en tiempo de despliegue. Y esto es, después de desplegar los ficheros, modificar el fichero index.html para añadir un parámetro con un valor aleatorio o temporal.

Así, en lugar de tener esto en index.html:

&lt;script src=&quot;./app/config.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;./app/controller.js&quot;&gt;&lt;/script&gt;

Tener esto:

&lt;script src=&quot;./app/config.js?random=3424&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;./app/controller.js?random=1427&quot;&gt;&lt;/script&gt;;

El como realizarlo dependerá del método de despliegue. En nuestro caso se encarga un script en BASH de realizar el despliegue y las tareas post despliegue. Después de descargar (y obviamente ofuscar y generar los ficheros .map) nuestro script de despliegue modificará index.html sustituyendo las referencias:

declare -a files=(&quot;config&quot; &quot;controller&quot;)

for file in &quot;${files[@]}&quot;
do
  echo &quot;Cambiando referencias a $file&quot;.js...
  sed -i &quot;s/&quot;$file&quot;.js/&quot;$file&quot;.min.js?random=$(( ( RANDOM % 1000000 )  + 1 ))/g&quot; $APP_ROOT/index.html
  echo '   '
done

De esta manera un fichero index.html siempre enlazara a aparentemente ficheros diferentes, hasta el siguiente despliegue. Como no cambiaran los ficheros .js sin despliegue pues el problema está solucionado.

3: Evitar la caché de index.html

El tema de index.html es lo mas sencillo ya que usualmente refrescando el navegador ya cogerá ese fichero, pero el usuario ya ha podido encontrarse con que la página no funciona.

Una solución que proponemos es:

  1. Crear un fichero version.txt que se actualiza en cada build y se aloja en la raíz.
  2. Añadir a index.html una etiqueta <meta name=”version” content=”SET_VERSION”> que irá con cada build.
  3. En el script de despliegue volcar el contenido de version.txt a el valor de esta etiqueta META
  4. echo 'Adding version to thoe index.html file'
    INSTALLED_VERSION=$(&lt;&lt;APP_ROOT/version.txt)
    sed -i &quot;s/SET_VERSION/$INSTALLED_VERSION/g&quot; $APP_ROOT/index.html
    
  5. Al abrir la aplicación, lo primero que hay que hacer (después de comprobar la versión del navegador, claro) es comprobar si la versión de index.html se corresponde con la version.txt
  6. function checkAppVersion(){
    		$.get('version.txt', function(fileVersion,  status, xhr ) {
    			 if ( status === &quot;error&quot; ) {
    				 var errorMsg = 
    					 'Ha habido un error comprobando la versión de servidor: ' +
    					 xhr.status + &quot; &quot; + xhr.statusText;
    				 alert(errorMsg);
    				 return;
    			 }
    			
    			var indexVersion = $(&quot;meta[name='version']&quot;).attr('content');
    			
    // Evitar el checkeo en el dominio local de desarrollo diadalocal.com
    			if(fileVersion !== indexVersion &amp;amp;amp;amp;&amp;amp;amp;amp; document.domain !== 'diadalocal.com'){
    				console.error('Version mismatch!&quot; fileVersion: ' + fileVersion + ', indexVersion: ' + indexVersion);
    				var msg = 
    					'Se ha detectado que la versión cargada en el navegador (' + indexVersion + 
    					') es diferente de la instalada en el servidor (' + fileVersion + 
    					'). Se va a proceder a refrescar la página';
    				
    				alert(msg);
    				location.reload(true)
    			} else {
    				// Añadir la versión en el footer
    				$('#footer').html('v' + fileVersion + ' | ' + $('#footer').html());
    			}
    			});
    	}
    

De esta manera, hemos evitado la cache a tres niveles. La máxima molestia que puede percibir el usuario es un mensaje informando de que hay un cambio de versión y un refresco de la pantalla.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.