Ayer me puse manos a la obra para crear un cargador de librerías, módulos, archivos JS o como sea que cada uno lo denomine. Desde un principio tenía claro que habría por ahí muchos ejemplos y cosas ya creadas, pero dado que no esperaba que se complicase el asunto, decidí ponerme por mi cuenta.
Como muchos sabéis, existen muchas maneras de lograrlo, todas ellas válidas. Sin embargo, ese pensamiento se borró rápido cuando se presentaba el siguiente escenario:
1 - Navegador Internet Explorer.
2 - Carga remota (fuera de "localhost").
Lo curioso es que buscando ejemplos y más ejemplos en Internet, comprobé que todos o casi todos caían en esa misma situación. Y los que no, se cargaban por completo la información tan valiosa de "debug" que uno obtiene a través de la consola de errores del navegador (porque usaban "eval").
Explico la situación... El plan más rápido es crear un archivo JS, enlazado en todas las páginas. Éste contiene una serie de instrucciones para cargar el resto de librerías JS (supongamos que todas están en la misma carpeta). El plan más lógico es que al ejecutarse se añadan objetos "script" al documento, sea por el método que sea:
A - Usando document.write ("<script ... ></script>").
B - Creando un objeto "script" mediante los métodos del DOM.
C - Usando document.getElementsByTagName ("<head>") [0].innerHTML += "<script ... ></script>".
Pues bien, Mozilla y Opera, aunque dispongas 20 sentencias así seguidas, esperan a la carga y validación de cada archivo JS para seguir con la página. Sin embargo, IE (al menos la versión 7), parece que no trata de forma síncrona esos elementos cargados en vivo y sigue con el resto de la página simultáneamente. Ignora por completo la propiedad "defer" de "script". Ya sea no escribiéndola en la etiqueta o establecíendola a false si usamos la creación por el DOM, no surte efecto alguno.
Que falle o no depende bastante del escenario debido precisamente a esa asincronía. Para probarlo lo mejor es pulsar rapídisimo la tecla de recarga (F5) con unos archivos con bastante código en un servidor remoto. ¿Qué ningún usuario haría eso? Quizá, pero bastaría una ralentización casual en la descarga para que saltasen los errores. Aparte, que mejor no dejar la funcionalidad al azar...
Por lo tanto, ese método queda descartado por completo si deseamos una compatibilidad 100% con TODOS los navegadores.
Pues bien, la única estrategia para forzar la carga síncrona de librerías viene a través de AJAX, mediante XMLHttpRequest. Lo que se suele hacer es descargar el archivo y emplear "eval (req.responseText)" para cargar los objetos en tiempo real. ¿Inconveniente? Que este método se carga por completo las ventajas de las consolas de error de los diferentes navegadores, ya que cualquier error de las librerías cargadas se asigna a la línea donde se encuentra "eval" y el tipo de error es interpretado como "EvalError".
Dándole vueltas y vueltas a este asunto hoy he llegado a una solución híbrida que funciona al menos en Mozilla, Opera, IE y Safari y que permite seguir recurriendo a la consola de errores.
Este ejemplo, totalmente personal, tiene en cuenta que mis librerías están en "sys/js" y que este JS se llama "load.js". Podéis modificarlo para cargar librerías por parámetro. En este caso yo las he especificado en el propio código y las cargo en el acto.
NOTA: CÓDIGO INCORRECTO. VER SOLUCIÓN MÁS ABAJO
Código:
// Crea la clase. La uso porque me gusta tener todas las // funciones bien clasificadas. No requerirá instanciación. function JSLoader () { } // Método de carga. JSLoader.load = function () { // Matriz con los nombres de las librerías deseadas. var arr_lib = ["cssquery", "core", "client", "init", "flash", "check", "form", "vfx", "vfxds"]; // Busca objetos "script" ya cargados en el documento, selecciona // aquel correspondiente al propio "load.js" y obtiene la ruta base // en la que se encuentra, ya que los archivos desde los que se llama // a "load.js" pueden estar en diferentes ubicaciones. var darr_script = document.getElementsByTagName ("script"); var urlbase = ""; for (var i = 0; i < darr_script.length; i++) { if (darr_script [i].src.indexOf ("sys/js/load.js") != -1) { urlbase = darr_script [i].src.replace ("load.js", ""); break; } } // Cea un objeto para AJAX y uno script. var req = new XMLHttpRequest (); var script = document.createElement ("script"); script.type = "text/javascript"; // Recorre la matriz de librerías. for (var i = 0; i < arr_lib.length; i++) { // Descarga el contenido de la librería de forma síncrona. req.open ("GET", urlbase + arr_lib [i] + ".js", false); req.send (null); // Carga y valida el contenido de la librería en memoria. Emplea // una sentencia "try" para que, en caso de que haya un fallo en // el código de una de las librerías, la ejecución continúe hasta el // siguiente punto y no provoque un error. try { eval (req.responseText); } catch (_ex) { } // Crea un nuevo objeto "script" que enlaza a la librería. Esto no // es necesario para cargarla, pues ya lo está, pero nos permite // que en caso de fallo, el navegador obtenga la traza en la consola // de error, con su número de línea. No entra en conflicto con los objetos // ya creados, ya que si algo ya existe, se sobreescribe sin más. script.src = urlbase + arr_lib [i] + ".js"; darr_script [0].parentNode.appendChild (script.cloneNode (true)); } }; // Ejecuta el método. JSLoader.load ();
Claves: importación, importador, cargador, librerías, módulos, loader, jsloader, ajax, script, import