HTML 5 Application Cache: Aplicaciones Offline

Si bien uno de los grandes motivos de anteponer las aplicaciones nativas sobre las aplicaciones web era la posibilidad de que estas primeras pudieran seguir trabajando a pesar de no disponer de Internet o acceso a un servidor determinado, con la especificación de HTML 5 que vamos a ver hoy cambia totalmente el panorama: Application Cache.

¿Qué es Application Cache?

Esta especificación está pensada para escenarios donde podemos experimentar situaciones en las que no disponemos de conectividad, como en un avión, zonas con poca cobertura, desconexiones intermitentes, etcétera. Hasta ahora con el modo de caché habitual no eramos capaces de obtener recursos a los que no hubiéramos accedido previamente, ni podíamos indicar a qué páginas era posible navegar aún cuando no disponíamos de acceso al servidor, por falta de Internet o porque el mismo no estuviera disponible en ese momento. Pero no sólo los escenarios relacionados con la conectividad son los candidatos de disfrutar de esta API, sino que también nos permite mejorar rendimiento del servidor reduciendo las peticiones de recursos en usuarios recurrentes, manteniendo los más estáticos cacheados en el navegador de forma explícita.

Esta característica está siendo muy utilizada en diferentes escenarios como juegos (como la aplicación de Angry Birds de Chrome Web Store), aplicaciones offline de acceso al correo electrónico, clientes web de música por streaming como Deezer y todas aquellas que puedan trabajar en local y sean capaces de sincronizar su contenido más tarde, o necesiten reducir el flujo de peticiones.

¿Cómo funciona?

Para poder trabajar con App Cache lo primero que debemos conocer es el archivo llamado cache manifest. Se trata de un archivo de texto plano con la siguiente estructura:

CACHE MANIFEST
#version 5.9

CACHE:
#This section list all resources that should be downloaded and store locally

#HTML files
index.html

#JavaScript files
/Scripts/appCache.js
/Content/images/glyphicons-halflings.png
/Scripts/bootstrap.min.js
/Scripts/jquery-1.9.1.min.js

#CSS files
/Content/bootstrap.min.css

NETWORK:
#This section list all URLs that may be loaded over Internet.
online.html

FALLBACK:
#Lists replacements for network URLs to be used when the browser is offline or the remote server is unavailable
/home.html /offline.html
/Content/images/glyphicons-halflings-white.png /Content/offline.png

En primer lugar, todo manifiesto debe comenzar con CACHE MANIFEST en la primera línea del archivo, de lo contrario podemos encontrar problemas cuando se intenta procesar por el navegador.

También es importante tener cuidado con los códigos de control que inserta Visual Studio al crear este fichero dentro de una solución, por lo que recomiendo guardar el mismo como Unicode (UTF-8 without signature) – Codepage 65001 a través de File –> Advanced Save Options.

Todas aquellas líneas que comiencen con una almohadilla (#) se tratan de comentarios. A continuación revisamos las siguientes secciones del manifiesto:

CACHE
Listado de todos aquellos recursos que deben ser descargados y almacenados localmente.
NETWORK
Lista de todas las URL que necesitan acceso al servidor remoto/Internet. Es posible utilizar el asterisco para indicar que cualquier recurso que no esté listado en el apartado CACHE necesita acceso a la red para acceder a él. Es posible hacer uso de URLs relativasy absolutas.
FALLBACK
En el caso de no tener acceso al servidor remoto se pueden especificar qué recursos deben responder por aquellos que no pueden ser recuperados. Los mismos pueden ser de cualquier tipo (una página HTML, una imágen, código JavaScript, etcétera). El primer parámetro es el recurso al que nos gustaría acceder y el segundo el que tomará su lugar cuando existan problemas de conectividad.
¡Nunca almacenes en cache el propio manifiesto!

Para indicar que un sitio tiene un manifiesto relacionado, basta con asociarlo en la etiqueta html de la página:

<!DOCTYPE html>
<html manifest="manifest.appcache">
<head>

El mismo puede tener el nombre y extensión que se prefiera, pero con convención se sugiere *.appcache. Como MIME type del mismo es recomendable servir text/cache-manifest. Si trabajamos con aplicaciones .NET podemos establecerlo a nivel del archivo web.config de la siguiente forma:

<?xml version="1.0"?>

<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->

<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
  <system.webServer>
    <staticContent>
      <mimeMap fileExtension=".appcache" mimeType="text/cache-manifest"/>
    </staticContent>
  </system.webServer>
</configuration>

Ciclo de vida de Application Cache

Una vez que la página asociada al manifiesto haya cargado, lo primero que el navegador comprobará es si tiene la versión más reciente del manifiesto para ese sitio. De no ser así descargará del servidor la última versión. Una vez obtenida comenzará a descargar todos los recursos nombrados en el apartado CACHE y aquellos que podrían ser utilizados como FALLBACK, en el caso de que no pudiéramos alcanzar el servidor de destino. A partir de este momento se trabajará con los recursos cacheados y no volverán a descargarse hasta que el manifiesto vuelva a sufrir una nueva actualización o limpiemos la datos temporales de nuestro navegador.
Si quisiéramos comprobar su efectividad, basta con desconectar el dispositivo de Internet e intentar navegar a aquellas páginas y recursos que fueron nombrados en el manifiesto. Del mismo modo, si intentáramos acceder a aquellos recursos que poseen un fallback, el navegador nos desviaría directamente a aquellas URL indicadas como segundo parámetro.

Es muy importante recordar que aunque alguno de los recursos listados en el manifiesto sufran modificaciones los navegadores de los clientes no dispondrán de esos cambios hasta que la fecha de modificación del manifiesto se vea alterada (Es buena práctica indicar la versión del manifiesto en un comentario para forzar la modificación del archivo si los recursos no varían en número y sólo en forma). App Cache tampoco auto refresca la página y esto hará que los últimos cambios descargados por el navegador no sean aplicados hasta la próxima carga del sitio. Por lo tanto, será necesario forzar la recarga de la página de forma manual o automática basándose en los eventos que producen esta característica.

Eventos

En las versiones más recientes de Chrome (En el momento del post Chrome 26) es posible ver los eventos ocurridos a través de la consola de la herramienta de desarrollo:

App-Cache-Events

No obstante, es posible añadir manejadores a cada uno de ellos con el siguiente código JavaScript:

window.onload = function () {

    //App Cache
    if (window.applicationCache) {
        console.log("Application cache is available");

        var appCache = window.applicationCache;

        appCache.addEventListener("checking", appCacheHandler, false);
        appCache.addEventListener("downloading", appCacheHandler, false);
        appCache.addEventListener("noupdate", appCacheHandler, false);
        appCache.addEventListener("obsolete", appCacheHandler, false);
        appCache.addEventListener("progress", appCacheHandler, false);
        appCache.addEventListener("error", appCacheHandler, false);
        appCache.addEventListener("updateready", appCacheHandler, false);
        appCache.addEventListener("cached", appCacheHandler, false);

        function appCacheHandler(event) {
            var status = appCache.status;
            var msg = null;

            switch (status) {
                case appCache.CHECKING:
                    msg = "<span class='label label-warning'>CHECKING</span>";
                    break;
                case appCache.DOWNLOADING:
                    msg = "<span class='label label-info'>DOWNLOADING</span>";
                    break;
                case appCache.UNCACHED:
                    msg = "<span class='label label-inverse'>UNCACHED</span>";
                    break;
                case appCache.OBSOLETE:
                    msg = "<span class='label label-important'>OBSOLETE</span>";
                    break;
                case appCache.IDLE:
                    msg = "<span class='label label-info'>IDLE</span>";
                    break;
                case appCache.UPDATEREADY:
                    msg = "<span class='label label-success'>UPDATEREADY</label>";
                    break;
                default:
                    msg = "UNKNOWN STATUS";
                    break;
            }

            var log = document.getElementById("log");
            log.innerHTML += '<li> Event: <span class="label label-inverse">' + event.type + '</span> Status: ' + msg + "</li>";

            if (status === appCache.UPDATEREADY) {
                appCache.swapCache();
                if (window.confirm('App cache was updated, you need refresh this page... May I?'))
                    window.location.reload();
                else {
                    log.innerHTML += '<li><span class="label label-important">You need to refresh this page to see the changes!</span></li>';
                }
            }

        }
    }
};
Las clases CSS que he utilizado para mostrar la información pertenecen al framework Bootstrap de Twitter.

Lo primero que comprobamos en nuestro código es si Application Cache está soportado por nuestro navegador (las últimas versiones de los navegadores más populares ya lo implementan). Para tener una visión más completa de la disponibilidad de esta característica podemos consultar esta tabla.

En cuanto a los eventos tenemos los siguientes:

checking
Se lanza cuando el navegador está comprobando la última versión del manifiesto alojada en el servidor remoto.
downloading
Indica que el manifiesto ha sido descargado y comienza la obtención de los recursos.
noupdate
Indica que tenemos la última versión del manifiesto y no es necesario descargar ningún recurso.
obsolete
La versión del servidor está obsoleta.
progress
Se lanza por cada uno de los recursos incluídos en la sección CACHE
error
Como su propio nombre indica, se trata del evento reservado para los errores durante la obtención de los recursos indicados en el manifiesto.
updateready
Nos permite conocer cuándo todos los recursos han sido actualizados con la nueva versión. En este ejemplo, además de mostrar el texto correspondiente, hacemos una comprobación al terminar el switch donde verificamos si hemos llegado a este punto y forzamos el cambio de cache a los nuevos elementos a través de la función swapCache(). Si bien esta función nos ahorra una recarga en el sitio, swapCache no nos permite servir los recursos actualizados de manera automática, por lo que debemos preguntar al usuario si podemos refrescar la página para aplicar los nuevos cambios.
cached
Los recursos han sido descargados y la aplicación ha sido cacheada. Este evento sólo ocurre la primera vez que se cachea el contenido. En las siguientes descargas será updateready el que se lance.
También es posible solicitar la actualización de la cache a través de script (sin tener que refrescar la página) utilizando window.applicationCache.update(), aunque será necesario igualmente el swap de la caché y el refresco de la página para poder aplicar los últimos cambios. Suele utilizarse cuando el usuario ha estado trabajando durante mucho tiempo en el sitio sin tener que refrescar la página y recuperar así el nuevo manifiesto durante una recarga.

En este ejemplo la misma función controla todos los eventos posibles y, a través de un switch, muestra además el estado que refleja cada uno de ellos donde curiosamente no tiene por qué coincidir con el nombre del evento. Podemos verlo en la siguiente imagen:

App-Cache-demo

Eventos Online y Offline

Por último, aunque no pertenezca a la API de App Cache, me gustaría mostrar la forma de conocer cuándo una aplicación no dispone de acceso a Internet… ¡Efectivamente! Existe otra API para ello 😀 En este caso debemos hablar de los eventos online y offline pertenecientes al objeto window. A través de la propiedad navigator.onLine podemos conocer cuál es nuestro estado actual e incluso podemos manejar los eventos online y offline de la siguiente manera:

    //Internet status
    function isOnline() {
        return navigator.onLine;
    }

    function internetStatus() {
        var className = isOnline() ? "badge-success" : "badge-important";
        var statusName = isOnline() ? "online" : "offline";

        var internet = document.getElementById("internet");
        internet.className = "badge " + className;
        internet.innerText = statusName;
    }

    window.addEventListener("online", internetStatus, false);
    window.addEventListener("offline", internetStatus, false);

    internetStatus();

Como podemos ver en el ejemplo, hemos encapsulado el valor de [window.]navigator.onLine en la función isOnline con el objetivo de comprobar dentro de internetStatus cuál es su valor y asociar una clase y un texto a una etiqueta con id “internet” en nuestra página. Además asociamos esta función a los eventos online y offline de window para que, cada vez que ocurran, se vuelta a comprobar el estado de nuestro acceso a Internet. Por último se llama a internetStatus una primera vez al cargar la página para conocer en qué estado nos encontramos inicialmente.

Podéis encontrar más información y demos en la página Appcache Facts.
Existe una página llamada Cache Manifest Validator que nos permite comprobar si hemos creado nuestro manifiesto correctamente. De hecho, también disponemos de una extensión para Chrome que realiza la misma función.

Espero que haya sido de utilidad 😀

¡Saludos!