jueves, 5 de diciembre de 2013

Web Workers


En esta entrada veremos la implementación de un Worker de HTML5 muy sencillo pero con implicaciones de conocimiento que vale la pena aprender.

Los Worker se hicieron para ejecución de tareas asíncronas que se ejecutarán en un hilo ajeno al que está ejecutando la página web. Tan ajeno que desde el Worker no tenemos acceso al DOM de la página, es decir que toda la sincronización se hace a través de mensajes, de objetos que sean serializables. Por excelencia utilizaremos JSON.

El problema planteado es que necesitamos realizar peticiones cada 10 segundos a un endpoint el cual devolverá un valor estadísticamente único, simulando que debemos estar checando el servidor para ver si hay datos nuevos para actualizar la interfaz del usuario.

Para esto vamos a preparar nuestro entorno de desarrollo (VS2010 o VS2012):

  1. Crear un sitio web
  2. Agregar una carpeta llamada js
  3. Agregar un archivo index.htm
  4. Agregar un Generic Handler llamado GetRandom.ashx
  5. Agregar jQuery a la carpeta js
  6. Agregar dos archivos js a la carpeta: tools.js y randomWorker.js

El archivo tools.js será el que esté en comunicación con el randomWorker, éste último es el que realizará las peticiones a GetRandom.ashx para un valor nuevo.

Prepararemos nuestro archivo index.htm con el siguiente código:
<head> <title>Valor aleatorio a través de Worker</title> <script src="js/jquery-1.7.2.min.js" type="text/javascript"></script> <script src="js/tools.js" type="text/javascript"></script> </head> <body> <p>Este es el valor aleatorio generado desde el servidor: <span id="lblAleatorio"></span></p> </body>

Vamos a abrir nuestro randomWorker.js para programar la tarea asíncrona que se estará ejecutando cada 10 segundos:
self.onmessage = function (e) { var xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { postMessage(JSON.parse(xmlhttp.response)); } } var url = '../GetRandom.ashx'; xmlhttp.open('GET', url); xmlhttp.send(); }

Estamos creando una instancia de XMLHttpRequest para realizar las peticiones a GetRandom.ashx. ¿Por qué no utilizamos $.ajax?, porque jQuery está basado en el DOM y por lo tanto no puede ser utilizado desde un Worker.

Estamos asignando un manejador al evento onreadystatechange que se dispara cada vez que hay un cambio en el estado de la petición. Los valores para readyState son los siguientes:

0: Sin inicializar, es decir que el método open no ha sido llamado.
1: Cargando, ya fue abierto pero el método send no ha sido llamado.
2: El método send ya fue llamado.
3: La respuesta está siendo descargada.
4: Todas las operaciones han finalizado.

El atributo status establece un valor para el tipo de respuesta que se está recibiendo. Los códigos de estado pueden tener los siguientes valores:

1xx: El servidor ya recibió las cabeceras y se puede proceder con el envío del cuerpo de la petición.
2xx: La solicitud fue recibida por el servidor y fue correcta.
3xx: Para terminar la solicitud el cliente debe de tomar acciones adicionales. Generalmente encontraremos que se debe a que el recurso ya fue cambiado de dirección.
4xx: La sintaxis de la petición no es la correcta o bien esta no puede ser procesada. Esto indica un error del lado del cliente.
5xx: Ocurrió un error del lado del servidor.

Tomando en cuenta lo anterior, el readyState que nos interesa es el 4 y el status es el 200, es por ello que esa validación se está haciendo en el if.

Nuestro GetRandom.ashx estará devolviendo información en formato JSON por lo que procedemos a procesarla y con la función postMessage se la enviamos al cliente del Worker (tools.js)

Notemos también que no estamos utilizando la función window.setInterval para programar la petición cada 10 segundos, eso es debido a que en el Worker no tenemos acceso al DOM y por lo tanto no tenemos acceso al objeto window.

Abrimos nuestro archivo tools.js y colocamos el siguiente código:
var tools = tools || {}; tools.Worker = new Worker('js/randomWorker.js'); tools.Worker.onmessage = function (m) { $('#lblAleatorio').text(m.data.value); }; tools.Worker.onerror = function (e) { alert('Ocurrió un problema durante la ejecución del Worker'); } $(document).ready(function () { tools.Worker.postMessage('getRandomValue'); window.setInterval(function () { tools.Worker.postMessage('getRandomValue'); }, 10000); });

Estamos creando un espacio de nombres "tools" para que cualquier variable que utilicemos no entre en conflicto con otra que esté en la página. Después estamos creando una instancia del objeto Worker que será el encargado de ejecutar la tarea asíncrona establecida en el archivo randomWorker.js.

Estamos asignando un manejador al evento onmessage del objeto Worker que recibirá el resultado de la ejecución asíncrona del Worker. Notemos que el resultado viene en la propiedad data. El resultado de la ejecución la colocaremos en nuestro elemento span con el id lblAleatorio.

Por si hay algún error, estamos también estableciendo un manejador para el evento onerror. Al finalizar la carga de la página, estamos realizando una petición inicial y dejamos programada una nueva petición en intervalos de 10 segundos. El método postMessage requiere que le sea pasado un argumento, entonces estamos enviando un texto muy sencillo y que realmente en este ejercicio no tiene la mayor importancia porque de él no depende alguna ejecución por parte del Worker.

Finalmente vamos a programar nuestro GetRandom.ashx con el siguiente código:
public class GetRandom : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "application/json"; context.Response.Write("{\"value\": \"" + Guid.NewGuid().ToString() + "\"}"); } public bool IsReusable { get { return false; } } }

Veamos que el ContentType de la respuesta está establecido en el MIME que necesita JSON, también deberemos notar que estamos generando un valor estadísticamente único con el objeto Guid.

Voy a visitar el archivo index.htm con IE 11. Esto es debido a que quiero hacer notar algo en particular con este navegador:

Ejecución con Internet Explorer 11

Podemos esperar más tiempo pero veremos que el valor único no es actualizado, esto se debe al caché del navegador. ¿Cómo podemos probarlo?, vamos a abrir las herramientas de desarrollo presionando F12 y activando la captura de red:

Captura del tráfico de red con la herramienta para desarrolladores de Internet Explorer 11

¿Qué está pasando?, podemos ver que el status es 304, esto significa que la petición está siendo servida desde el caché del navegador y no está yendo hasta el servidor por el nuevo valor. Para corregir este detalle vamos a ajustar el código de GetRandom.ashx de la siguiente manera:
context.Response.ContentType = "application/json"; context.Response.AppendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); context.Response.AppendHeader("Pragma", "no-cache"); context.Response.AppendHeader("Expires", "0"); context.Response.Write("{\"value\": \"" + Guid.NewGuid().ToString() + "\"}");

Una vez realizado el cambio volvemos a visitar nuestro index.htm y veremos que cada 10 segundos el valor será actualizado.

Este ha sido un ejercicio muy sencillo pero aborda varias cosas que vale la pena notar:

  1. No tenemos acceso al DOM desde un Worker.
  2. No podemos utilizar jQuery en un Worker dado que jQuery está basado en el DOM.
  3. La comunicación entre el Worker y la página (en nuestro caso tools.js) debe ser a través de mensajes de texto. Es por ello que se recomienda el uso de JSON.
  4. Deshabilitamos el caché en nuestro Handler para evitar que el navegador no realice la petición al servidor al despachar el resultado desde el caché.

Espero les haya resultado de mucha utilidad, nos vemos la siguiente.

No hay comentarios.:

Publicar un comentario