Requirejs: El cargador de módulos para JavaScript

Dentro del mundo de la programación existe un alto porcentaje de desarrolladores que detestan trabajar con JavaScript 🙁 Existen diferentes motivos y hoy me quiero centrar en uno de los comunes, para contaros después una estupenda solución 🙂

JavaScript, archivos y dependencias entre ellos.

Para poder explicarlo he creado un pequeño ejemplo con una funcionalidad de lo más básica: He creado una página que me permite lanzar una serie de mensajes por la consola de desarrollo. Este sería el ejemplo de lo que espero:

ejemplo requirejs

El objeto encargado de hacer las llamadas a la función console será el siguiente:

var log = function () {
    var
        info = function (msg) {
            console.log(msg);
        },
        warning = function (msg) {
            console.warn(msg);
        },
        error = function (msg) {
            console.error(msg);
        };
    return {
        info: info,
        warning: warning,
        error: error
    };
}();

Pero no quiero utilizar el objeto log directamente, sino que me gustaría añadir un objeto más que me permita indicar el tipo de mensaje que quiero transmitir (obviamente no es un caso real, es para entender el escenario con algo muy básico :)). Para ello hemos creado un objeto trace que determina el tipo de mensaje y utiliza el objeto log para mostrarlo.

var trace = function () {
    return {
        message: function (msg, type) {
            switch (type) {
                case "info":
                    log.info(msg);
                    break;
                case "warning":
                    log.warning(msg);
                    break;
                case "error":
                    log.error(msg);
                    break;
                default:
                    return;
            }
        }
    };
}();

Para poder añadir el evento al botón y hacer uso de trace, tenemos un último archivo llamado bootstrapper.js:

(function () {
    
    var btn = document.getElementById("btn");
    btn.addEventListener("click", function () {
        var msg = document.getElementById('msg'),
            type = document.getElementById('type');
        trace.message(msg.value, type.value);
    });
})();

La página HTML que utilizamos para ejecutar este código JavaScript es la siguiente:

<!DOCTYPE html>
<html>
<head>
    <title>Bad solution</title>
</head>
<body>
    <section>
        <input type="text" id="msg" />
        <select id="type">
            <option>info</option>
            <option>warning</option>
            <option>error</option>
        </select>
        <button id="btn">Send</button>
    </section>
    <script src="scripts/3.log.js"></script>
    <script src="scripts/2.trace.js"></script>
    <script src="scripts/1.bootstrapper.js"></script>
</body>
</html>

He enumerado los archivos para que se vea claramente que los mismos deben tener un orden descendente. El motivo es bastante obvio: boostrapper no puede llamar a trace si no ha sido declarado previamente y trace no puede utilizarse hasta que exista el objeto log. Para comprobarlo basta con cambiar el orden de los scripts y comprobar desde la consola el error que supone el orden incorrecto en la carga:

ejemplo de error no definido-no requirejs

RequireJS

Debido a este caso en concreto, en aplicaciones con cierta carga en el cliente puede complicar tanto el desarrollo como la colaboración con otros desarrolladores, lo que hace que en ocasiones se descarte este lenguaje para muchos proyectos.
RequireJS está aquí para solucionar este inconveniente 😀 Se trata de una librería que nos permite cargar archivos en el orden correcto y bajo demanda. La aplicación se vuelve modular, aparecen dependencias entre unos archivos y otros, con lo que además mejora la calidad de nuestro código. Veamos el mismo ejemplo con Requirejs:

En este caso, vamos a comenzar por bootstrapper.js, donde hemos añadido información adicional:

(function () {
    requirejs.config({
        baseUrl: 'scripts2'
    });

    var btn = document.getElementById("btn");
    btn.addEventListener("click", function () {
        require(['2.trace'], function (trace) {
            var msg = document.getElementById('msg'),
                type = document.getElementById('type');
            trace.message(msg.value, type.value);
            
        });
    });
})();

En primer lugar, hemos indicado a través de la configuración de requirejs dónde están nuestros archivos JavaScript dentro de nuestra aplicación (en este caso en una carpeta llamada scripts2). Seguimos haciendo uso del botón pero hemos decorado el contenido con require, pasándole entre corchetes ‘2.trace’ y una función anónima donde queda ubicado el código que ya teníamos. ¿Qué significa? Si leemos a la inversa, lo que estamos diciendo es que para poder lanzar esa petición a trace.message es necesario (require) el módulo llamado 2.trace y, una vez que el mismo se cargue se podrá ejecutar dicho código. Como podéis imaginar, 2.trace es el archivo js que contiene nuestro objeto trace visto anteriormente, con algunas mejoras :):

define('2.trace', //Module name
    ['3.log'], //Dependencies
    function (log) { //Module implementation
        return {
            message: function (msg, type) {
                switch (type) {
                    case "info":
                        log.info(msg);
                        break;
                    case "warning":
                        log.warning(msg);
                        break;
                    case "error":
                        log.error(msg);
                        break;
                    default:
                        return;
                }
            }
        };
    });

Si comparamos este código con el anterior, lo que hemos hecho ha sido encapsular el contenido dentro de define, donde debemos indicar:

  • Nombre del módulo: Debe de coincidir con el nombre del archivo que lo contiene. De no ser así requirejs no lo encontrará. De hecho, se aconseja no indicar el nombre del modulo para que el mismo sea más portable y fácil de mover a otras ubicaciones.
  • Dependencias de este módulo: Podemos indicar el nombre de otros módulos que sean necesarios para la correcta ejecución del que estamos definiendo.
  • Implementación del módulo: Contenido del módulo. Para poder hacer uso de las dependencias tendremos tantos argumentos como dependencias hayamos indicado.

En este caso, tenemos como dependencia el archivo/modulo 3.log, donde la nueva implementación es similar que la de 2.trace:

define('3.log', //Module name
    [], //Dependencies
    function () { //Module implementation
        var
            info = function (msg) {
                console.log(msg);
            },
            warning = function (msg) {
                console.warn(msg);
            },
            error = function (msg) {
                console.error(msg);
            };
        return {
            info: info,
            warning: warning,
            error: error
        };
    });

En el caso de que un módulo no tenga dependencias de otros, simplemente indicamos un array vacío.

Por último el código HTML se ha visto reducido con este cambio:

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
    <section>
        <input type="text" id="msg" />
        <select id="type">
            <option>info</option>
            <option>warning</option>
            <option>error</option>
        </select>
        <button id="btn">Send</button>
    </section>
    <script data-main="scripts2/1.bootstrapper" src="scripts2/require.js"></script>
</body>
</html>

Si os dais cuenta, en el mismo sólo se está referenciando a require.js con una particularidad: utiliza el atributo data- de HTML 5 para poder indicar cuál es el punto de entrada de la aplicación (en este caso bootstrapper.js). También es posible referenciar los dos archivos por separado, requirejs y bootstrapper, pero creo que esta forma es más elegante 🙂

También es importante recalcar que los archivos se descargan bajo demanda, lo que supone una mejora en la carga de la página. Para poder probar este último punto, podemos ejecutar la aplicación con la pestaña de Red abierta y veremos que inicialmente se carga require.js y bootstrapper.js, pero trace.js y log.js sólo serán cargados cuando sean necesarios, es decir cuando se ha clic en el botón 😉

Espero que haya sido de utilidad.

¡Saludos!

Proyecto de ejemplo