Estoy trabajando en un proyecto que utiliza JavaScript del lado del cliente Específicamente las librerías de Ext-JS. Por cierto, javascript es horrible, pero eso lo explicare en otro post.
Necesitaba cargar clases de forma dinámica para no hacerlo todo en la única pagina que este proyecto utiliza. Pensé que seria simple pero me equivoque. Así es como había ideado el proceso de carga de clases.
- Obtener la única instancia de esta clase usando el método getInstance(). Tiene que ser un singleton ya que toda la aplicación compartirá el mismo namespace.
- Registrar las clases que se quieren cargar utilizando el método add(nombre_clase, url_script).
- Llamar al metodo start() para empezar a cargar las clases, una a la vez.
- Cada vez que un script haya sido cargado se disparara el evento onLoad que sera escuchado por la clase manager provocando que esta realiza un inventario de las clases cargadas, actualice la lista y continúe con la carga del siguiente script.
- Una vez se hayan cargado todas las clases el manager dispara el evento onComplete.
El problema estaba en que no hay manera de detectar en JS si una clase esta cargada o no.
Investigue en Internet pero no pude encontrar una solución acertada, así que decidí aprovecharme del scope de JS para detectar que script era el que había disparado el evento onLoad.
Cuando onLoad es llamado el código ejecutado tiene como this el elemento DOM que llama al método. Por lo tanto, si haces un this.src tendras como resultado la url del script que acaba de ser cargado.
Una de las cosas que mas me toca acostumbrarme en JS es el controlar el scope donde un metodo es llamado. Si tu escribes un this.algo dentro de un método no podrás saber donde apuntara ese this ya que el método sera ejecutado usando el scope de quien lo llama. This sucks, literalmente.
Bueno, luego de resignarme a que JS no es capaz de saber si una clase existe decidi usar el url del script como referencia. La clase resultante es la siguiente.
Ext.ns('ScriptsLoader');
/**
* Constructor.
* Este metodo no debe ser llamado directamente. Utilizar getInstance().
*
* @param {Object} Configuración inicial.
*/
ScriptsLoader = function(config){
//Asegurare de que se usar getInstance()
if (ScriptsLoader.byglobal==false)
throw('Este metodo no puede ser llamado directamente.');
if(Ext.isEmpty(config))
config = {};
this.enabled = false;
this.files = [];
//Registrando eventos que esta clase puede disparar
this.addEvents({
'complete':true,//this
'progress':true,//classname, url, step, total
'loaded':true,//classname, url
'cancel':true//classname, url, step, total
});
Ext.apply(this, config, {
autoLoad:false,//Cargar los scripts llamando a start()
basepath:base_url+"js/library/"
});
};
Ext.extend(ScriptsLoader, Ext.util.Observable, {});
/**
* Devuelve la unica instancia permitida de esta clase.
*
* @param {Object} config
* @return {Object}
*/
ScriptsLoader.getInstance = function(config){
if ( typeof ScriptsLoader.global == 'undefined' ) {
ScriptsLoader.byglobal = true;
ScriptsLoader.global = new ScriptsLoader(config);
ScriptsLoader.byglobal = false;
}
return ScriptsLoader.global;
};
/**
* Variable estatica usada para evitar que se creen instancias de esta clase.
*/
ScriptsLoader.byglobal = false;
/**
* Busca en la cola si el url dado ya ha sido solicitado antes.
*
* @param url URL del archivo consultado
* @type {Boolean}
* @return Devuelve TRUE si el archivo que se solicita ya ha sido pedido antes.
*/
ScriptsLoader.prototype.isAlreadyRequested = function(url){
for(var i=0;i<this.files.length;i++){
var item = this.files[i];
if(item.url==url)
return true;
}
return false;
};
/**
* Recibe un nombre de clase y a partir de ella genera el nombre del archivo
* que contiene el script que deseamos cargar.
*
* @param classname Nombre de la clase
* @type {String}
* @return URL generada
*/
ScriptsLoader.prototype.urlFromClassname = function(classname){
//var url = classname.replace(".","/");
var url = classname.split('.').join('/');
//var arr = classname.split(".");
return url+".js";
};
/**
* Agrega a la cola la carga de un script.
*
* @param classname Nombre de la clase
* @param url Opcional
* @type {Boolean}
* @return Devuelve TRUE si el script va a ser cargado y FALSE en caso contrario.
*/
ScriptsLoader.prototype.add = function(classname, url){
//Si no se da el url, se genera
if(Ext.isEmpty(url))
url = this.urlFromClassname(classname);
url = this.basepath+url;
//Este script ya ha sido solicitado antes?
if(this.isAlreadyRequested(url))
return false;
//Se pone al script en la cola
this.files.push({classname:classname, url:url, loaded:false});
if(this.autoLoad)
this.start();
return true;
};
/**
* Empieza la carga de scripts.
*/
ScriptsLoader.prototype.start = function(){
this.enabled = true;
this._loadNext();
};
/**
* Detiene el proceso de carga.
* Si una solicitud ya esta en curso, se espera a que esta acabe para detenerse.
*/
ScriptsLoader.prototype.stop = function(){
this.enabled = false;
};
/**
* Consulta si una clase ha sido registrada.
* Si este metodo devuelve TRUE, pueden crearse instancias con el nombre de la
* clase dada.
*
* @param classname
* @return {Boolean}
*/
ScriptsLoader.prototype.classExist = function(classname){
var type = eval("typeof("+classname+")");
if(type == "function")
return true;
else
return false;
};
/**
* Este metodo es llamado cada vez que un script es cargado.
* Su tarea es adivinar que script ha sido cargado. Logra esto
* haciendo un inventario de las clases que ya han sido cargadas.
*
* @param {Boolean} stopOnFirst Detenerse al primer encuentro. Por defecto es FALSE
* @type {Boolean} Devuelve TRUE si se encuentra una clase que no habia sido marcada como cargada.
*/
ScriptsLoader.prototype.onLoadScript = function(stopOnFirst){
//Detenerse al primer encuentro?
if(Ext.isEmpty(stopOnFirst))
stopOnFirst = false;
var ls = ScriptsLoader.getInstance();
var found = false;
//Recorriendo los scripts
with(ls){
for(var i=0;i<files.length;i++){
var item = files[i];
//Si una clase espera ser cargada
if(!item.loaded){
//Si se detecta que la clase ya puede ser instanciada
if(this.src == item.url){
item.loaded = true;
found = true;
fireEvent('loaded', item.classname, item.url);
if(stopOnFirst)
break;
}
}
}
//Cargar el siguiente script en la cola
_loadNext();
}
return found;
};
/**
* Carga un script.
*
* @param {String} url
* @type object
* @return Devuelve el DOM del script creado
*/
ScriptsLoader.prototype.loadScript = function(url){
var script = document.createElement('script');
script.setAttribute('src',url);
script.setAttribute('type','text/javascript');
var head = document.getElementsByTagName("head")[0];
head.appendChild(script);
script.onload = this.onLoadScript;//Para todos los browsers excepto IE
script.onreadystatechange = this.onLoadScript;//Para IE
return script;
};
/**
* Llama al metodo loadAll del manager siguiente en la lista.
* Este metodo no debe ser acedido externamente. Usar start() en su lugar.
*
* @returns {Boolean} Retorna TRUE si hay clases que cargar.
*/
ScriptsLoader.prototype._loadNext = function(){
for(var i=0;i<this.files.length;i++){
var item = this.files[i];
//Si la clase espera ser cargada
if(!item.loaded){
if(this.enabled){
//classname, url, step, total
this.fireEvent('progress', item.classname, item.url, i+1, this.files.length);
this.loadScript(item.url);
}else{
var cancelItem = this.files[i-1];
//classname, url, step, total
this.fireEvent('cancel', cancelItem.classname, cancelItem.url, i-1, this.files.length);
}
return true;
}
}
//Todo cargado
this.stop();
this.fireEvent('complete', this);
return false;
};
Veras que utilizo métodos de la clase Ext en este código. Es necesario porque al extender esta clase de Ext.util.Observable, heredo la habilidad de poder disparar eventos.
Este código esta bajo licencia CC con permiso para realizar modificaciones y uso con fin comercial (detalle completo de la licencia), por lo tanto, puedes copiarlo y modificarlo como creas conveniente.

ScriptsLoader by Daniel Zegarra is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
Permissions beyond the scope of this license may be available at http://danielzegarra.net/.


