Incluyendo ficheros Javascript bajo demanda (includes)

18 May 2011

Dentro de la programación web es muy frecuente desarrollar una estructura modular compuesta por varios ficheros que vamos llamando según los necesitemos. Por ejemplo, todos conocemos el ejemplo de los includes o requires en PHP que nos permiten incluir ficheros dentro de otros. Esta técnica es especialmente útil en el marcado HTML para separar elementos comunes de las páginas como un header o un footer en archivos independientes que solo hay que llamar cuando se necesiten.

Sin embargo, pese a que la mayoría de lenguajes incorporan de uno u otro modo esta funcionalidad (la de llamar e incluir otros archivos dentro de él mismo), Javascript no la presenta de forma nativa. Es por ello que existen numerosas bibliotecas que expanden al lenguaje permitiendo estas inclusiones con un alto grado de control. Una de las más completas al respecto puede ser Require.JS a la cual dedicamos un artículo hace ya algún tiempo. jQuery, por su parte, permite también este comportamiento a través del método getScript() que tanto vimos en el pasado.

NOTA: Actualmente, gracias a la granularidad de los plugins en jQuery y a los patrones de expansión de módulos ya no resulta tan frecuente encontrar el getScript() como lo era en el pasado.

Como solemos hacer en este blog, si no queremos que nuestro código presente dependencias de terceros o simplemente queremos el control total, podemos implementar nuestra propia función para añadir ficheros Javascript a una página bajo demanda emulando la funcionalidad de estas bibliotecas o los comandos de otros lenguajes.

El código

El código para implementar la función resulta sumamente sencillo:

function loadScript(url, callback) {
  var script = document.createElement('script');
 
  if (script.readyState) { // IE
    script.onreadystatechange = function () {
      if (script.readyState === 'loaded' || script.readyState === 'complete') {
        script.onreadystatechange = null;
        callback();
      }
    };
  } else { // Others
    script.onload = function() {
      callback();
    };
  }
 
  script.src = url;
  document.getElementsByTagName('head')[0].appendChild(script);
}

Como podemos observar, solo tenemos que hacer la distinción entre navegadores ya clásica: Explorer por un lado y todos los demás por otro.

La mágia de este snippet es añadir de forma dinámica una nueva etiqueta script a la cabecera de nuestra página (el head) con la dirección URL del recurso a cargar. De forma adicional, incluimos un callback al estilo jQuery que se ejecutará tan pronto como nuestro nuevo script esté disponible.

NOTA: Para aquellos que gusten de añadir los script al final de sus páginas buscando la carga asíncrona de los componentes y demás, sólo tienen que cambiar el TagName ‘head’ por ‘body’.

Ejemplo de uso

Probemos nuestra función cargando la última versión de jQuery desde el CDN de Google:

var myLibrary = 'http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js';
 
loadScript(myLibrary, function(){
  alert('jQuery is ready!');
});

Si ejecutamos el código anterior, en cuanto el script se cargue y esté disponible, recibiremos el alert definido en el callback avisando de ello.

Conclusión

Pues poco más que añadir: un pequeño código homemade que puede resultar muy útil para importar nuestros recursos Javascript en tiempo de ejecución según vayan siendo necesarios para el desarrollo de nuestra aplicación.

Más:

{7} Comentarios.

  1. aanton

    Una pregunta al maestro 🙂

    ¿Merecería la pena mejorarlo con el patrón modulo (revelado), un control interno para evitar cargas duplicadas, …? O en ese caso mejor usar una de las bibliotecas existentes (require.js, yepnope, …)

    Saludos!

    • Carlos Benítez

      Pues siempre depende de lo complejo que sea el escenario.
      Para evitar cargar duplicados, podríamos poner simplemente un condicional:

      if(!jQuery){
        // Rutina para cargar script
      }
      

      Si la cosa es más compleja y hay muchas dependencias cruzadas, lo más interesante es una arquitectura modular (revelada o no) expandible mediante submódulos-plugins.

      Si para implementar esto, hay que reescribir toda una app que ya está hecha, pues si que podemos tirar de RequireJs o Yepnope…

  2. Valentin

    Tampoco sería demasiado lo que tenés que cambiar para evitar cargas duplicadas y de mejor manera que comprobar una variable global:

    function loadScript(url, callback) { 
    	if(loadScript.cache.indexOf(url) != -1) {
    		callback();
    	}
    	//... resto de la implementacion
    }
    
    loadScript.cache = [];
    
    • Carlos Benítez

      En principio, esa técnica de caché es válida pero solo evitaría duplicados declarados desde el mismo método, no globales. Por eso utilicé una comprobación general en mi ejemplo anterior.
      Si por ejemplo jQuery ha sido definido dentro del marcado normal, esta modificación no detectaría el duplicado en runtime por lo que no funcionaría como se espera.

      Saludos!

  3. Valentin

    Claro, en lo personal creo que si se usa un sistema de dependencias se lo debe usar en toda la aplicación.

    Saludos.

  4. yeikos

    if(!window.jQuery){
    // Rutina para cargar script
    }

  5. Lucas

    Les dejo mi aporte:

    function include() {
    this.idx = -1;
    this.args = arguments;

    (this.nextLoad = function() {
    this.idx++
    if (this.idx >= this.args.length) return true;

    if (typeof this.args[this.idx] == ‘function’) {
    this.args[this.idx]();
    this.nextLoad();
    } else
    loadScript(this.args[this.idx], this.nextLoad);
    })();
    }

    include(“http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js”, jQueryLoaded,
    “http://code.createjs.com/easeljs-0.4.2.min.js”,
    “http://code.createjs.com/tweenjs-0.2.0.min.js”,
    “http://code.createjs.com/movieclip-0.4.1.min.js”,
    “http://code.createjs.com/preloadjs-0.1.0.min.js”,
    “http://code.createjs.com/soundjs-0.2.0.min.js”, init);

    function jQueryLoaded() {
    alert($);
    }

    function init() {
    alert(‘Init Project’);
    }

Deja un comentario

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