Yepnope.js y la carga condicional de recursos en Javascript

09 Mar 2011

Hay ocasiones, muy de vez en cuando, en que aparece una librería tan bien construída que, con solo un vistazo, sabes que va a populizarse inmediatamente. Eso es lo que me ha pasado recientemente con yepnope.js, en principio una herramienta más para añadir archivos a nuestras aplicaciones bajo demanda, pero que rápidamente comienza a aportar cosas nuevas.

Sobre la Librería

Es cierto de que existen muchas alternativas en Javascript cuando se trata de cargar selectivamente archivos de tipo CSS o JS dependiendo según alguna condición previa. En este mismo blog, hemos comentado en alguna ocasión herramientas como RequireJs o HeadJS, sin embargo Yepnope se hace querer desde el primer momento.

Desarrollada por Alex Sexton (autor de Modernizr) y Ralph Holzmann (Groupcard), ocupa menos de 2Kb en los que encierra una rica API construida de forma modular. Algunas de sus características más interesantes son:

  • Integra QUnit para dirigir las pruebas y comprobar que todo funciona según lo previsto.
  • Permite añadir nuevas funcionalidades mediante un sistema de plugins.
  • Su funcionamiento es completamente asíncrono y los recursos siempre se cargan siguiendo el orden especificado.

El proyecto se encuentra en su versión 1.0.1 y podemos descargarlo directamente desde su página en Github.
Toda la documentación está disponible en la web oficial de Yepnope.

Ejemplo rápido de uso

La sintaxis de Yepnope es una mezcla entre los frameworks de test más frecuentes tipo TDD y el estilo jQuery de método-argumentos-callback.

Un ejemplo simple de uso sería:

yepnope({
  test : Modernizr.geolocation,
  yep : 'normal.js',
  nope : [ 'polyfish.js', 'wrapper.js' ]
});

Como podemos comprobar, el ejemplo resulta fácil de leer:

test: la condición, el objeto que evaluamos.
yep: si la condición se cumple, cargamos el recurso(s) asociado.
nope: si la condición no se cumple. cargamos la alternatica.

Todas las propiedades son opcionales, lo que permite una granularidad muy fina a la hora de escoger cómo actuar en cada caso posible.

Flexibilidad

Además de las propiedades anteriores, tenemos a nuestra disposición otras muy interesantes:

both: tanto si la condición se cumple como si no, se cargan los recursos indicados.
load: permite cargar directamente un recurso(s) independientemente de los tests.
callback: función que se ejecuta una vez que los recursos han sido cargados.
complete: función que se ejecuta una vez la evaluación ha finalizado sin que necesariamente se haya cargado un recurso.

Un ejemplo más claro de todo lo anterior puede ser el siguiente:

yepnope({
  // Load jQuery from a 3rd CDN
  load : 'http:/­/code.jquery.com/jquery-1.5.0.js',
 
  // The object test
  test : window.JSON,
 
  // If the test fails
  nope : 'json2.js',
 
  // When json2.js is loaded
  callback : function(){
    console.log( 'JSON not present by default but now is ready to use' );
  },
 
  // When the test is complete (both true or false)
  complete : function(){
    var data = window.JSON.parse( '{ "foo" : "Hello World" }' );
  }
});

Del código anterior, las propiedades más importantes son el callback y el complete que pueden resultar algo confusas a priori. Veamos la primera de ellas un poco más en detalle:

yepnope({
  load: ['script1.js', 'style1.css'],
  callback: function (url, result, key) {
    // Wait! What happens in here now?! Does this get called once or twice?
  }
});

En este ejemplo extraído de la documentación nos encontramos con que cuando se cargan varios recursos, el callback se ejecuta tantas veces como archivos se llamen. Esto, a priori, podría entenderse como un problema pero, comprendido su funcionamiento, se convierte en una poderosa característica. La sintaxis del callback incorpora tres argumentos que tenemos que conocer:

callback: function (url, result, key) {
  console.log(url, result, key);
}

Donde:
url: Es la url que ha sido cargada.
result: Es el resultado del test.
key: Es la clave que identifica al elemento concreto que se ha cargado. Para los casos en que sólo carguemos un fichero, su valor es 0.

Con estos valores, podemos manejar a nuestro antojo los callbacks y tomar las decisiones oportunas. El siguiente ejemplo muestra cómo funcionan:

yepnope({
  test: Modernizr.geolocation,
  yep: 'regular-styles.css',
  nope: {
    'mstyles': 'modified-styles.css',
    'geopoly': 'geolocation-polyfill.js'
  },
  callback: function ( url, result, key ) {
 
    // Testing the url
    if ( url === 'modified-styles.css' ) {
      console.log( 'The Styles loaded!' );
    }
 
    // Testing the test
    if ( result ) {
      console.log( 'The Test Passed!' );
    } else {
      console.log( 'The test Failed!' );
    }
 
    // Testing the file
    if (key === 'geopoly') {
      console.log('This is the geolocation polyfill!');
    }
 
  }
});

Como podemos comprobar, en lugar de un array hemos pasado un objeto con su clave-valor a la propiedad nope, lo que nos da la pista en el callback del archivo que estamos tratando en cada iteración.

La propiedad complete, a diferencia de la anterior, se cargará una vez que todos los recursos y sus respectivos callbacks se hayan ejecutado:

epnope({
  test: Modernizr.geolocation,
  yep: {
    'rstyles': 'regular-styles.css'
  },
  nope: {
    'mstyles': 'modified-styles.css',
    'geopoly': 'geolocation-polyfill.js'
  },
  callback: {
    'rstyles': function (url, result, key) {
      console.log('This is the regular styles!');
    },
    'mstyles': function (url, result, key) {
      console.log('This is the modified styles!');
    },
    'geopoly': function (url, result, key) {
      console.log('This is the geolocation polyfill!');
    }
  },
  complete: function () {
    console.log('Everything has loaded in this test object!');
  }
});

Integración con Modernizr

Los autores de esta librería han buscado en todo momento la integración con otra herramienta sencillamente genial: Modernizr.

Modernizr añade una serie de clases a la etiqueta HTML de cada página para determinar las funcionalidades especifícas del navegador en el que se visualiza. Esto permite aplicar estilos CSS concretos a cada elemento sin necesidad de utilizar Javascript o trucos complicados.

Para comprender su funcionamiento, echemos un vistazo a la etiqueta HTML que genera Modernizr en una página cargada con Firefox 4:

<html class="  js flexbox canvas canvastext webgl no-touch geolocation postmessage  no-websqldatabase indexeddb hashchange history draganddrop no-websockets  rgba hsla multiplebgs backgroundsize borderimage borderradius boxshadow  textshadow opacity no-cssanimations csscolumns cssgradients  no-cssreflections csstransforms no-csstransforms3d csstransitions  fontface video audio localstorage no-sessionstorage webworkers  applicationcache svg inlinesvg smil svgclippaths" lang="en">

Si tenéis la paciencia suficiente para desplazar la barra de scroll del ejemplo anterior, observaréis que se encuentran mencionadas todas las características que soporta el navegador. Esto facilita la creación de estilos CSS específicos:

.multiplebgs div p {
  /* properties for browsers that
     support multiple backgrounds */
}
.no-multiplebgs div p {
  /* optional fallback properties
     for browsers that don't */
}

Sencillamente genial. No son necesarios hacks ni artefactos raros que luego son difíciles de testear: según las clases que Modernizr incluye en la etiqueta HTML, se aplica una regla de estilo u otra.

Para nuestros proyectos, dependiendo de sus necesidades, podemos descargar una versión de Modernizr personalizada que no incluya una comprobación de todos los elementos posibles, lo que finalmente es interesante para cuando solo tenemos que evaluar si el navegador soporta bordes redondeados o, por ejemplo, sombra en los textos. Durante la descarga, se nos permite además selecciónar la opción de integración con Yepnope.

Modernizr Loader

Con esta posibilidad, utilizar ambas librerías en un mismo proyecto se vuelve transparente.

yepnope({
  test: Modernizr.inputtypes.email &&
        Modernizr.input.required &&
        Modernizr.input.placeholder &&
        Modernizr.input.pattern,
  nope: 'h5f.min.js',
  callback: function(url, result, key) {
    H5F.setup(document.getElementById("signup"));
  }
});

El ejemplo anterior ha sido extraído del artículo de Louis Simoneau, Regressive Enhancement with Modernizr, en el que nos muestra un ejemplo de uso real.

En este caso concreto, se trata de un formulario elaborado con algunas de las nuevas funcionalidades del HTML5 (campos requeridos, placeholders, patrones de validación, campos de email, …). Si el navegador no dispone de las características necesarias para ejecutar correctamente el formulario, se carga la librería de Ryan Seddon H5F que las emula en aquellos navegadores en que no están presentes forma nativa.

De este modo, podemos controlar a bajo nivel tanto el aspecto como las funcionalidades de una página web aplicando la técnica que llamamos “optimización regresiva”.

Conclusión

En la web oficial del proyecto, podemos encontrar muchos más ejemplos, plugins y toda la API desarrollada. Sin embargo, con lo que hemos expuesto, debería ser suficiente para que nos hagamos una idea de la potencia y flexibilidad de esta librería a la hora de seleccionar las dependencias de nuestras aplicaciones cara a desarrollar interfaces más usables tanto en entornos multiplataforma como multinavegador.

Más:

{5} Comentarios.

  1. matias

    muy buen tuto, en el primer ejemplo el test deberia ser window.JSON no?

    • Carlos Benítez

      Tienes razón Matias; el método JSON va en mayúsculas. Ejemplo actualizado.

      Muchas Gracias!

  2. matias

    Alguna idea de porque en la solapa net de firebug aparecen los recursos cargados dos veces?

  3. matias

    bueno, en la página oficial explica bastante bien porque aparece dos veces
    “Due to the nature of how yepnope works, there are two requests made for every file. The first request is to load the resource into the cache and the second request is to execute it (but since it’s in the cache, it should execute immediately).”

    gracias por el tuto, me encanta tu sitio y leo la mayoría de tus feeds

    • Carlos Benítez

      Estaba escribiendo la respuesta cuando la has encontrado tu primero 🙂
      Efectivamente, hay dos peticiones por cada fichero: una para la carga y otra para la ejecución. La segunda actúa sobre un contenido cacheado, por lo que se realiza de forma inmediata.

      Gracias por el feedback!

Deja un comentario

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