cursor

Queríamos compartir con ustedes algunas mejoras que implementamos en la nueva versión del Panel de Control de Mango.

Este es el primero de una serie de artículos en los que vamos a estar compartiendo diferentes técnicas que implementamos para mejorar la experiencia en el Panel de Mango utilizando nuevas tecnologías web.

Expiración de sesiones (es por tu bien)

Parte de las medidas de seguridad que utilizamos en el Panel de Control desde el primer momento incluyen el uso de sesiones de usuario que expiran luego de 15 minutos de inactividad.

Esta nueva versión del Panel de Control de Mango incorporó funcionalidades en las cuales los usuarios pueden estar un tiempo prolongado analizando métricas, reportes de sus transacciones o realizando acciones que pueden no necesariamente notificar a nuestros servidores.

Por dicho motivo implementamos un mecanismo de envío de mensajes de keep-alive para avisarle a los servidores de Mango que la sesión sigue activa (esto contempla eventos de mouse, teclado, etc.).

Este mecanismo también se encarga de expirar la sesión si ningún evento es disparado durante dichos 15 minutos. Antes de desactivar la sesión automáticamente, el usuario tiene la opción de «mantener viva» la sesión clickeando un modal que advierte su pronta expiración.

bert

Luego de realizar diferentes pruebas, nos dimos cuenta que la técnica utilizada para expirar la sesión tenía dos puntos débiles en relación a la experiencia del usuario:

  • Si un usuario tenía abierto el panel en más de una pestaña (tab), la actividad de una pestaña no era registrada por sus pestañas hermanas. Es decir, darle más vida a una instancia del panel en una pestaña no afectaba el tiempo de vida client-side del resto.
  • El usuario no tenía forma de saber si el tiempo estaba por agotarse en caso de estar navegando otras pestañas u otras aplicaciones que no estén relacionadas al Panel de Mango.

La experiencia de usuario es uno de nuestros pilares como compañía y es por eso que pensamos técnicas para poder mitigar estos puntos.

El problema de las «tabs hermanas»

Para conocer el uso actual de las distintas instancias del panel (tabs) al mismo tiempo y sincronizar los tiempos de vida de las sesiones, acudimos a un truco de un viejo amigo: localStorage (IE8+).

Un uso poco conocido de la API de localStorage es que el objeto window emite el evento storage al resto de los tabs con el mismo esquema (según CSP).

Este truco es muy útil a la hora de enviar mensajes entre las tabs que controlamos. Utilizando esta API del navegador es muy simple notificar a las otras instancias de la aplicación cada vez que un evento de keep-alive se dispara en un tab.

// Para que se ejecute el evento 'storage', se debe setear un valor distinto al anterior.
// Es por eso que usamos 'new Date()'.
localStorage.setItem('keep-alive', (+new Date()) + '');

...

// En otra parte de la aplicación
window.addEventListener('storage', function(eve) {
  if (eve.key === 'keep-alive' && isActive()) {
    refreshSession();
  }
}, false);

De este modo, manteniendo «viva» una sola sesión nos aseguramos de mantener el resto «vivas» también.

Notificaciones para todos

Gran cantidad de navegadores ya implementaron un método para poder notificar a los usuarios de eventos importantes cuando no están activamente usando nuestra aplicación o incluso cuando tienen el navegador minimizado.

Se llaman Web Notifications y varias aplicaciones ya están comenzando a utilizar este patrón aprovechando su gran poder para mejorar la experiencia de aquellos usuarios que utilizan navegadores modernos.

Las Web (o Desktop) Notifications nos permiten disparar notificaciones nativas del sistema operativo a través del navegador. A continuación podrán ver nuestra notificación previo a que expire la sesión:

desktop-notification-v2

Con una porción muy pequeña de código (como la del ejemplo a continuación) lograremos captar la atención del usuario para que realize la acción de «mantener viva» la sesión en caso de que quiera hacerlo. Adicionalmente, tenemos la posibilidad de redireccionar al usuario directamente a la pestaña en la que se emitió el evento cuando hace click en la notificación.

const desktopNotification = (title, options={}) => {
  if (document.hidden && 'Notification' in window) {
    const notification = new Notification(title, options);
    notification.onclick = () => { try { window.focus(); } catch(e) {} };
    return notification;
  }
};

Conclusión

Es muy importante conocer y tener en cuenta las tecnologías disponibles hoy en día y aprovecharlas al máximo para mejorar la experiencia de nuestros usuarios. En nuestro caso, de manera simple pudimos implementar dos soluciones que tienen un valor agregado para la experiencia de los usuarios en nuestro producto.

En el próximo artículo hablaremos sobre como utilizando nuevas tecnologías podemos achicar la brecha entra las aplicaciones web y el mundo móvil.

Cualquier duda/comentario o experiencias que quieran compartir, nos pueden contactar vía Twitter a @impronunciable o @pazguille.

Funciones async en JavaScript

El comité TC39, encargado de la estandarización de ECMAScript (JavaScript para los amigos), incluyó en la versión ES2016/ES7 las llamadas Async Functions. Esta nueva característica, proveniente de C#, permite estructurar nuestro código asíncrono utilizando sintaxis del mundo síncrono, facilitando la manera en que pensamos el flujo de la aplicación.

Utilizando el caso de los llamados XHR vamos a mostrar la evolución en el trato del código asíncrono en JavaScript para llegar a esta nueva construcción llamada «Async Functions».

En el comienzo todo era callback

Hace mucho tiempo, en una galaxia muy muy lejana, la gente de Microsoft inventó una tecnología que luego se haría conocer como AJAX. Como los requests al servidor son muy lentos y el navegador solo permitía el uso de un thread para nuestro código (el mismo thread de UI), realizar un request xhr y esperar por la respuesta para seguir con el flujo de la app no es una opción ya que bloquearía el uso del resto de la aplicación para el usuario. Es por eso que cada vez que ejecutábamos/ejecutamos un request utilizando el objeto XMLHttpRequest pasamos una función (o más) a ejecutar cuando la respuesta es recibida.

Usando jQuery para el ejemplo, un pedido AJAX al servidor se ve así:

function pedirPartidos(competencia, success, error) {
  $.ajax({
    "url": "/api/partidos.json",
    "success": success,
    "error": error,
    "data": {
      "competencia": competencia      
    }
  });
}

function copaAmerica() {
  pedirPartidos('Copa America', function(data) {
    if(!data.juegaMessi) mostrarError(new Error('No se transmite'));
  }, mostrarError);
}

Este approach, que es el más utilizado hasta la actualidad, tiene algunos problemas. En principio el flujo de la función tiene ramas donde el órden de ejecución no se condice con el órden del código dentro de la misma. El programa va a ejecutarse en este órden:

  • Request a /api/partidos.json
  • Termina la ejecución de copaAmerica y el programa sigue ejecutando otras tareas y escuchando eventos.
  • Retorna el llamado a la función:
    1. Si el request fue exitoso se ejecuta la primera función pasada por parámetro y si todo está bien, luego se llama a prepararLaTele.
    2. Si falló el request se ejecuta la segunda función de error.

Por otra parte la función copaAmerica no puede retornar el valor de la data pedida al servidor ya que esta termina su ejecución antes de que el request pueda ser completado.

Además, si otro request depende de la respuesta del primero, tengo que llamarlo desde la función de callback, facilitando lo que se conoce como «callback hell».

Callback hell

Zona de promesas

Una respuesta a esta falta de estructura es la introducción de un objeto llamado «Promise» o (Promesa), con su implementación estrella Promises/A+. La idea de las promesas es mitigar algunos de los problemas de usar callbacks para operaciones async:

  1. Retorna un objeto que «promete» ser completado
  2. Hace uso de excepciones

Volviendo a nuestro ejemplo de la Copa América, podemos reescribirlo utilizando promesas. (Los métodos de AJAX de jQuery retornan promesas desde la versión 1.5).

function pedirPartidos(competencia) {
  return $.getJSON("/api/partidos.json", { "competencia": competencia });
}

function copaAmerica() {
  var promesaPartidos = pedirPartidos('Copa America')
  .then(function(data) {
    if(!data.juegaMessi) throw new Error('No se transmite');
    prepararLaTele(data, 'HD'); // Salió bien
  })
  .catch(function(error) {
    mostrarError(error);
  });

  return promesaPartidos;
}

En este caso vemos como las funciones ahora sí retornan valores, pudiendo pasar promesas entre secciones del programa. La función que llame a copaAmerica va a poder hacer uso de promesaPartidos y a la vez no es necesario el pasaje de callbacks entre funciones.

Otro punto de interés tiene que ver con la capacidad de las promesas de emitir errores y handlearlos. Esta es una mejora circunstancial respecto del uso de callbacks donde no es posible.

Promises/A+ fue incluido en el estándar de ES2015 (ES6), es decir que ya está en la última versión terminada del lenguaje y ya se puede usar en algunos navegadores sin la necesidad de librerías o polyfills.

Async/Await al rescate

Si bien las promesas ayudan en gran parte al problema del flujo de código asíncrono, no resuelven completamente, el mindset sigue siendo parecido al del uso de callbacks. Para mejorar la experiencia a la hora de desarrollar se va a incluir en el próximo estándar llamado ES2016, la estructura llamada «Async Functions». Gracias al uso de Promises y la introducción de generadores en el lenguaje, sumado a 2 nuevas keywords «async» y «await» podemos repensar como escribimos código async.

Nuevamente vamos a reescribir el ejemplo, ahora utilizando una función async:

function pedirPartidos(competencia) {
  return $.getJSON("/api/partidos.json", { "competencia": competencia });
}

async function copaAmerica() {
  try {
    var partidos = await pedirPartidos('Copa America');
    if(partidos.juegaMessi) throw new Error('No se transmite');
     prepararLaTele(data, 'HD'); // Salió bien
     return partidos;
  } catch(e) {
    mostrarError();
  }
}

Ahora sí, nuestro código tiene estructura de código síncrono y el valor de retorno de la función es simplemente la información que necesitamos y que puede ser recogida por otra función async. La estructura try/catch (de ES3) nos permite handlear errores sin necesidad de uso de estructuras ad-hoc.

¿Qué pasa si ahora quiero pedirle al servidor información de 2 competencias en paralelo y devolver cuando estén completas?

async function copaAmerica() {
  var partidos = await*([pedirPartidos('Copa América'), messi('Argentino B')]);
  console.log(partidos); // [{//data de copa america}, {//data de argentino b}]
}

Esta nueva estructura definitivamente nos permite pensar el código async como sync, simplemente utilizando funciones de tipo async.

Tengo que esperar hasta 2050?

No es necesario esperar a que los navegadores adopten esta feature para poder usarla hoy. De hecho si entran ahora al Panel de Control de Mango, van a ejecutarse requests utilizando esta técnica.

Gracias a transpilers como Babel pueden usar async/await en navegadores como IE8. En el caso de Babel, es necesario habilitar la transformación es7.asyncFunctions.

Otro caso de uso

Un caso de uso muy interesante es el uso de funciones async para el acceso a bases de datos en node.js.

El siguiente es un ejemplo, pidiendo prestadas las arrow functions de ES6 y utilizando express y el ORM Sequelize (que hace uso de promesas):

// antes
app.get('/partidos', (req, res) => {
  Partido.findAll({})
  .then(function(partidos){
    res.json(partidos);
  });
});

// usando async functions
app.get('/partidos', async (req, res) => res.json(await Partido.findAll({}) ));

Podemos concluir que esta nueva construcción nos permite creer que estamos ejecutando código de modo síncrono, pero como toda magia es solo una ilusión.

Cualquier duda/comentario o experiencias que quieran compartir, me pueden contactar vía Twitter a @impronunciable.

slideout

En Mango siempre nos esforzamos en diseñar la mejor experiencia para nuestros usuarios a la hora de utilizar nuestros productos desde cualquier dispositivo. Ya sean celulares, tablets o computadoras de escritorio.

Nuestro panel de administración es una single-page application que construimos aplicando el concepto de Mobile first donde el proceso de diseño empieza a pensarse comenzando por el uso de dispositivos móviles y luego se va adaptando a otros dispositivos. Además de pensar en diferentes resoluciones, hay que pensar en las diferentes capacidades con las que contamos en cada dispositivo y utilizarlas para poder brindar la mejor experiencia posible.

En este caso, quiero contarles como aprovechando los eventos touch creamos una experiencia mucho más fluida en nuestro panel. Seguramente, cuando usan una aplicación nativa, están acostumbrados a abrir un menú arrastrando la pantalla desde un costado.

En las últimas semanas estuvimos trabajando para llevar esa interacción a nuestra web app y estamos muy contentos de anunciar Slideout.js.

Slideout.js es un módulo open source que ofrece la posibilidad de utilizar el patrón off-canvas en tu sitio o aplicación web de una forma muy sencilla aprovechando al máximo los eventos touch, de una manera muy fluida, sin dependencias y sólo en 4 Kb.

Slideout.js demo

Los invito a que lo prueben desde sus dispositivos touch y si están interesados nos ayuden a mejorarlo: testeando, cargando issues o agregando mejoras.

¡Hasta la próxima!

A very important part of the build phase of a single-page app it’s the bundling process, which consists in combining the app’s files and modules needed in order to run it.

In Mango’s Dashboard we used Browserify to structure the front-end.

Browserify provides lots of advantages and useful features as we outlined in this post.

The simplest bundling process using Browserify has an output of a single JavaScript file that contains the whole application logic.

The problem with single bundles

As new features are added to the app, the size of the bundle also increases and create some inconvenients:


– Download time increases.
– Time to interpret the code also increases.

Our Dashboard -like most single-page apps- needs to download, parse and interpret code before rendering the views.
This creates longer waiting periods before being able to use the app, directly affecting the user experience.

The first problem -although real- it does’t manifest in a linear fashion as the output code increases. As TJ VanToll points out, gzip compression is very powerful and works better with larger files (the bigger the file, the more repetitions it needs to find).

The bigger problem relies with the parsing and interpreting timeframe of the initial code.

Filament Group made an interesting analysis about the penalisation of using certain popular MVC frameworks that provides a clear vision of the problem.

factor-bundle to the rescue

factor-bundle works as a Browserify plugin that receives multiple files as entry points (in our case are mapped to the sections of our app), analyses their content and generates two things: an output file for each input file, and also an extra file that contains the related dependencies between them.

Thanks to this ‘factorization’ of dependencies, when users load the app, only downloads the JavaScript associated to the section that they’re visiting and the related dependencies. The remainder sections will only download in an asynchronic way and without repeating the dependencies downloaded while the users navigate the Dashboard.

A small optimization we make is for each output file creating another file with the shared dependencies. This avoids downloading an extra script (-common-.js).

The output after the process looks like this:

.
|-- config-0.3.6.js
|-- config-common-0.3.6.js
|-- login-0.3.6.js
|-- login-common-0.3.6.js
|-- store-single-0.3.6.js
|-- store-single-common-0.3.6.js
|-- stores-0.3.6.js
|-- stores-common-0.3.6.js
|-- user-0.3.6.js
`-- user-common-0.3.6.js

There are optimisations to delay downloading not used modules, i.e. detecting mouse proximity to links to different sections or similar strategies. We decided to talk about these optimisations in future posts.

Conclusions

Separating bundles resulted in an optimization of waiting times of around 30% when users logged in to our Dashboard; thanks to the decreased size of the initial script and also the fewer amount of code to interpret. Also, adding new sections doesn’t impact the performance of the app’s initial load.

There are a lot of techniques to enhance the initial load of single-page apps. One we’re investigating right now is the shared rendering between client and server. A very interesting advantage of bundle factorization is that it’s almost exclusively a change in the compilation process of the app.
Business logic and rules remained intact.

Any doubt, comment or experiences you want to share, please contact me via Twitter: @dzajdband.