Blog de Daniel Zegarra Rotating Header Image

noviembre, 2010:

Cargar scripts de JavaScript en runtime

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.

  1. 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.
  2. Registrar las clases que se quieren cargar utilizando el método add(nombre_clase, url_script).
  3. Llamar al metodo start() para empezar a cargar las clases, una a la vez.
  4. 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.
  5. 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.

Licencia de Creative Commons
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/.

Trabajar con PHP y Flex en el mismo Framework

Usar PDT 2.0 y Flash Builder 4.1 en el mismo framework es algo muy simple de lograr ya que ambos son plugins de Eclipse.

Los pasos para instalar PDT como plugin en una instalación previa de Eclipse se pueden encontrar aquí.

Advertencia: Flash Builder esta basado sobre la versión 3.4 de Eclipse (alias Ganymede), por lo tanto, se debe usar la versión 2.0 de PDT preparada para esta versión.

Segun la wiki de eclipse, estos son los pasos a seguir:

  1. Ir al menu Help > Software Updates… > Available Software > Manage Sites…
  2. Usando el boton Add, agregar la direccion http://download.eclipse.org/technology/dltk/updates-dev/1.0M4-PDT-2.0/
  3. Nuevamente, agrega la direccion http://download.eclipse.org/tools/pdt/updates/2.0/
  4. Presiona el boton Manage Sites y habilita el sitio de actualización de Ganymede (http://download.eclipse.org/releases/ganymede/) si es que no se encuentra habilitado. Los sitios habilitados son los marcados con el check.

    Activando el sitio de actualización de Ganymede

    Activando el sitio de actualización de Ganymede

  5. Ahora debes seleccionar que paquetes vas a instalar. Selecciona los siguientes paquetes:
    En DLTK / Dynamic Languages Toolkit / Seleccionar Dynamic Languages Toolkit - Core Frameworks

    Seleccionando DLTK Core
    Seleccionando DLTK Core


  6. En PDT Update Site / PDT SDK 2.0.1 / Seleccionar PDT Runtine Feature
    Seleccionando PDT

    Seleccionando PDT

    Nota: En las imagenes no tengo seleccionado el paquete descrito porque yo ya lo tengo instalado.

  7. Hecho esto, dale un clic en el botón Install.
  8. Acepta la condiciones y espera a que termine la instalación. Cuando te pida reiniciar Eclipse lo haces.

Suerte en tu proyecto!

    Error del garbage collector en distribuciones Linux que tienen a Debian como base

    Hay un bug inquietante en la instalación por defecto de PHP sobre servidores Debian y distribuciones que parten de él (como Ubuntu). Aparentemente el error aparece de forma aleatoria y con el siguiente mensaje:

    session_start() [function.session-start]: ps_files_cleanup_dir: opendir(/var/lib/php5) failed: Permission denied (13)

    Si vuelves a ejecutar refrescar la página el error ya no aparece. Con el tiempo llega a ser desesperante :P
    Bueno, si tu te has encontrado mas de una vez con este insecto te explico porque este error se dispara y como solucionarlo.

    Las sesiones de php son datos asociados a un cliente que visita el servidor. Cuando un visitante se conecta por primera vez al servidor este le asigna un codigo para identificar al mismo cliente durante su visita. El cliente entrega este codigo en cada comunicacion de tal forma que el servidor pueda reconocer al visitante y a su vez asociar informacion adicional a este codigo sin que el cliente lo sepa. Para entenderlo mas facilmente, el servidor le asigna al visitante un casillero numerado y le entrega el numero de la llave para reconocerlo mas tarde.
    En servidores web de produccion, que pueden tener miles de visitantes en tan solo unos minutos, almacenar los codigos de sesion en memoria resulta una mala idea, por lo que almacena todos estos codigos en el disco duro (quedando los mas nuevos o mas utilizados en la RAM del servidor).
    Cuando un visitante deja de comunicarse con el servidor por un largo tiempo, el servidor entiende que ese visitante ya se ha retirado y procede a anular su sesion, pero la informacion asociada a esta aun queda en el disco duro. PHP esta configurado para depurar regularmente las sesiones inactivas y asi no llenar el disco duro de basura.
    El problema aparece aqui. En los servidores Debian las sesiones son almacenadas en un directorio con permisos restringidos y estas son depuradas por el cron del sistema. Entonces, cuando PHP intenta realizar la depuracion se produce un error debido a que PHP no tiene los permisos suficientes sobre el directorio de sesiones.

    La solucion es muy simple: Decirle a PHP que no depure las sesiones. Dejar que cron lo haga.
    Para esto abre y modifica la siguiente linea en el archivo de configuracion de PHP (php.ini). En Ubuntu lo puedes encontrar usualmente en /etc/php5/apache2/php.ini. (no olvides abrirlo con permiso de escritura).

    $config['gc_probability'] = 1;

    No recuerdo el numero de linea ahora. Debe estar ubicado en la seccion de parametros de configuracion de sesiones.
    Cambia el valor de gc_probability a 0 para que PHP nunca depure las sesiones olvidadas.

    Guardar los cambios, reinicias apache y listo.

    La solucion fue hallada gracias a este hilo de discusión:
    http://forum.kohanaframework.org/discussion/565/garbage-collector-error-with-sessions-on-debian/p1

    Depurar Code Igniter con Eclipse y Zend Debugger

    Sin hacer cambios a Code Igniter no se puede. Aun si enable_query_strings esta definido en FALSE Code Igniter leera las variables que PDT pasa usando GET para que Zend Debugger conecte correctamente a la consola. Es una tonteria que Eclipse no nos permita NO pasar estas variables via GET. La solucion mas rapida pero no permanente es abrir el sitio que deseamos depurar en un browser aparte luego de tener a Eclipse esperando la conexion del depurador, pero es una salida molesta.

    Lo que hice fue decirle a Code Igniter que ignore ciertas variables entregadas via GET aprovechando los Hooks para no tocar el codigo base de CI.

    Para usar este hook sigue estos pasos:

    1. Descarga este archivo y guardalo en tu escritorio.
    2. Descomprime el contenido del archivo en el directorio application de tu instalacion de CI. Si ya tienes registrado algun hook entonces no reemplaces el archivo config/hooks.php. Agrega el contenido del archivo descargado tu archivo hooks.php actual.
    3. Asegurate que enable_hooks es igual a TRUE en el archivo config.php ubicado en CIroot/application/config/.

    Luego de hacer esto, intenta depurar tu aplicacion. Deberia correr sin problemas y ya no mostrar el error de que no se encuentra el controlador.

    Nota: La intencion de este articulo es solo compartir un archivo fuente para utilizar Zend Debugger con CI. Para informacion de la instalacion de Zend Debugger o su uso con Eclipse PDT  puedes… googlear.

    Descargar ZendDebugger&CI Hook