La Página de DriverOp

Cargar dinámicamente archivos JavaScript.

En este artículo se verá cómo cargar un archivo .js dinámicamente, especialmente útil para ejecutar código JavaScript luego de una petición Ajax.

Índice

Planteamiento del problema

Habrá sucedido más de una vez que se necesita cargar un archivo JavaScript no al momento de cargarse la página sino después de que la página ha sido cargada.

El caso paradigmático de esta situación es cuando se quiere que código JavaScript que está en un archivo HTML se ejecute después de que ese archivo se cargó mediante una petición Ajax.

Lo que se habrá oido o leído muchas veces es que "Ajax no ejecuta JavaScript" y es realmente cierto: código JavaScript que está embebido en un archivo HTML no se ejecuta si éste se carga mediante Ajax.

Entonces ¿cómo se soluciona este problema?. Pues la solución es separar el código ejecutable JavaScript del código HTML poniéndolo en archivos separados y cargar el archivo .js (que contiene el código JavaScript) dinámicamente.

En este artículo se verá cómo realizar esta operación.

Ventajas.

La ventaja de cargar código JavaScript dinámicamente reside en no saturar el servidor pidiéndole material que no es seguro que se vaya a usar.

Por ejemplo, supongamos que un sitio tipo blog ofrece la posibilidad de comentar los artículos y se dispone de un formulario para que el visitante escriba el comentario.

No todos los visitantes del sitio comentarán el artículo. Por lo que cargar el formulario y el código JavaScript asociado para todos independientemente de si hará el comentario o no, podría ser un desperdicio de ancho de banda.

En cambio, si el visitante solicita publicar un comentario y el formulario se carga mediante Ajax, esa petición sí será útil.

Pero como se comentó al inicio, Ajax no ejecuta JavaScript y parece ser que no queda más remedio que cargar el o los archivos .js al inicio de la página como se hace normalmente con el tag <script>

Con el método que se verá a continuación esto no será necesario.

Manos a la obra.

Básicamente el método consiste en usar JavaScript para crear un nuevo tag <script> y ponerlo dentro de la etiqueta <head>.

Esto se consigue en dos simples pasos como sigue.


	var tagjs = document.createElement("script");
	document.getElementsByTagName("head")[0].appendChild(tagjs);

En el primer paso, se usa el método createElement() del objeto document para crear un nuevo tag <script> y se lo asiga a la variable local tagjs.

A continuación se usa el método getElementsByTagName() para acceder al array de elementos que son parte del tag <head>, como este método devuelve un array y como solo puede haber un solo tag <head> en el documento, usamos el primer (y con suerte el único) elemento del array que el método devuelve. Inmediatamente se usa el método appendChild() para agregar el tag que se acaba de crear como parte de <head>.

Esto no es suficiente ya que, como se podrá ver fácilmente, en ninguna parte se está indicado qué archivo del servidor se debe cargar puesto que no hay nada en el código que así lo indique.

Para esto hay que indicarle las propiedades del tag <script> que se crea con createElement(). Además hay que indicar el tipo MIME del archivo a cargar para que el navegador entienda que se trata de un script en JavaScript[1] y así pueda ejecutarlo después de la carga.

Entonces reformaremos el código anterior para contemplar esto:


	var tagjs = document.createElement("script");
	tagjs.setAttribute("type", "text/javascript");
	tagjs.setAttribute("src", "/miarchivojavascript.js");
	document.getElementsByTagName("head")[0].appendChild(tagjs);

Aquí usamos el método setAttribute() para primero indicar que el tipo MIME es "text/javascript" y que la fuente es el archivo que nos interesa cargar indicando su URL. En el ejemplo el archivo está en la raiz del servidor (tal como se denota con la barra inclinada) pero podría estar en cualquier otra parte del árbol de directorios del servidor[2].

Los atributos deben establecerse antes de agregar el nuevo tag al DOM del documento, de lo contrario no tendrán efecto. Ignoro por qué esto es así. Pero mis pruebas empírica así me lo dicen.

Para probar lo hecho hasta aquí y como lo que se quiere es cargar el archivo JavaScript bajo demanda, vamos a poner este código dentro de una función.


function LoadJS() {
	var tagjs = document.createElement("script");
	tagjs.setAttribute("type", "text/javascript");
	tagjs.setAttribute("src", "/miarchivojavascript.js");
	document.getElementsByTagName("head")[0].appendChild(tagjs);
}

Y se invocará desde el evento onClick de un <button>.

Esto puede ser probado en esta página de ejemplo.

No es un camino sencillo.

Si se ha probado el ejemplo, se verá que la cosa funciona y funciona muy bien. Pero al mismo tiempo se podrán notar un par de comportamientos poco deseables.

Hacer clic repetidamente en el <button> de la página de ejemplo dará como resultado que el archivo .js se carga múltiples veces. En la gran mayoría de los casos esto supone un problema y debe ser corregido.

Hay varias formas de evitar este comportamiento. Lo intuitivo y directo es que una vez de ha hecho clic en el botón, deshabilitar el botón para que el visitante solo pueda hacer clic una vez, sin embargo sería mejor si el propio código de carga se encargara del tema.

El tag <script> no es más que otro tag dentro del DOM y, según el estandar HTML, todo tag puede tener un atributo "id" con el cual referenciarlo en el DOM. El tag <script> no es la excepción.

Lo que se propone como solución a este problema es agregar al tag <script> un "id" único al momento de crearlo. Pero antes de esto comprobar que no existe ningún tag con ese "id" ya que de existir significaría que el archivo .js ya se cargó.

Para conseguir esto se hará uso del método getElementById() como sigue:


function LoadJS() {
	var ele = document.getElementById("mijsdinamico");
	if (ele == undefined) {
		var tagjs = document.createElement("script");
		tagjs.setAttribute("type", "text/javascript");
		tagjs.setAttribute("id", "mijsdinamico"); // esto es nuevo
		tagjs.setAttribute("src", "miarchivojs.js");
		document.getElementsByTagName("head")[0].appendChild(tagjs);
	}
}

Primero se trata de referenciar un elemento cuyo "id" sea "mijsdinamico", luego se pregunta si la variable "ele" está indefinida implicando que no existe un elemento cuyo "id" sea el que se busca. Si efectivamente no existe significa que el archivo JavaScript que nos interesa no ha sido cargado aún y por lo tanto se procede a cargarlo dinámicamente. Aquí, luego de crear el tag <script> correspondiente y además de establecer sus atributos como ya se vio en la sección anterior de este artículo, se agrega otro atributo más, el "id" cuyo valor será "mijsdinamico".

Esto evita efectivamente que el archivo JavaScript se cargue más de una vez. Es importante que el valor de "id" sea único para todo el documento HTML (es decir, no debe haber otro tag HTML que tenga ese "id") que se está haciendo, de lo contrario, como se podrá imaginar, las cosas no resultarán como se esperan.

La nueva versión del código puede ser probada aquí.

Cuando digo dinámico, quiero decir DINAMICO!

El último código visto hasta ahora es suficiente para la gran mayoría de los casos. Resuelve, por ejemplo, el caso de la carga del formulario de comentarios de un blog que se comentó al inicio de este artículo.

Sin embargo el avispado lector se habrá dado cuenta que la función LoadJS() solo sirve para cargar el mismo archivo .js. Como este artículo trata sobre dinamismo, sería bueno que una misma función sirviera para cargar cualquier archivo .js que sea menester.

Para el programador experimentado, reformar la función es bastante trivial. Simplemente se agregará la posibilidad de indicarle a la función qué archivo cargar mediante parámetros (no solo el nombre del archivo, también el "id" correspondiente).

Los cambios necesarios resultarán en un código como éste:


function LoadJS(nomarch, nomid) {
	var ele = document.getElementById(nomid);
	if (ele == undefined) {
		var tagjs = document.createElement("script");
		tagjs.setAttribute("type", "text/javascript");
		tagjs.setAttribute("id", nomid); // no te olvides del "id", que sí, que soy un pesado...
		tagjs.setAttribute("src", nomarch);
		document.getElementsByTagName("head")[0].appendChild(tagjs);
	}
}

Y los ejemplos de llamadas serían:


<button type="button" onClick="LoadJS('miarchivojs1.js','mijs01');">Cargar miarchivojs1.js!</button>
<button type="button" onClick="LoadJS('miarchivojs2.js','mijs02');">Cargar miarchivojs2.js!</button>

Pequeñas mejoras sobre lo anterior podrían ser omitir la extensión del archivo puesto que ya se sabe que el archivo a cargar tendrá extensión .js.


		tagjs.setAttribute("src", nomarch+".js");

También podría asumirse el lugar en el servidor remoto, suponiendo que todos los archivos JavaScript están en el mismo directorio.


		tagjs.setAttribute("src", "/js/"+nomarch+".js");

Finalmente podríamos ahorrarnos el segundo parámetro de la función y asumiendo que el "id" que se le asignará al tag es el mismo nombre que el archivo que se prentende cargar dinámicamente.[3]

Este es el código propuesto:


function LoadJS(nomarch) {
	var ele = document.getElementById(nomarch); // ahora el nombre del archivo es el ID
	if (ele == undefined) {
		var tagjs = document.createElement("script");
		tagjs.setAttribute("type", "text/javascript");
		tagjs.setAttribute("id", nomarch); 
		tagjs.setAttribute("src", "/js/"+nomarch+".js");
		document.getElementsByTagName("head")[0].appendChild(tagjs);
	}
}

Y los ejemplos de llamadas para la nueva versión serían serían:


<button type="button" onClick="LoadJS('miarchivojs1');">Cargar miarchivojs1.js!</button>
<button type="button" onClick="LoadJS('miarchivojs2');">Cargar miarchivojs2.js!</button>

Las cuales se ven más limpias.

Esta nueva versión puede ser probada aquí y se puede considerar realmente dinámica ya que se encarga por sí sola de muchos de los detalles de implementación, pero la vida real no siempre es ideal.

“¡Bravo, las cosas se complican!”[4]

El próximo problema a considerar no lo es tanto en el uso normal de esta función sino más bien al momento de desarrollar el sitio que la usará. Como supongo sabrá, cuando se crea un sitio web lo normal es que vayamos probando el nuevo código para ver si realmente hace lo que esperamos que haga. Ahora bien, si lo que se está haciendo es probar código JavaScript y éste se está cargando con esta función, casi con seguridad se notará que el código que se ejecuta no es el mismo que está en el archivo que se acaba de modificar y cuyas modificaciones son las que se desean probar, ¿por qué sucede esto?.

El responsable de este extraño fenómeno es la caché del navegador. Si se carga un archivo y luego se quiere cargar de nuevo aunque se haya refrescado la página, el navegador "ve" que se intenta cargar el mismo archivo, entonces no lo solicita al servidor, lo saca de su propia caché.

Esto es un problema serio a la hora de desarrollar y puede serlo en ciertos casos para el sitio en producción. Entonces la pregunta es ¿cómo hacer para que el navegador ignore su caché y siempre solicite una copia fresca al servidor?.

Tan grave como puede parecer el problema, su solución es bastante sencilla: agregar un parámetro aleatorio en el atributo "src" del tag <script> que se crea dinámicamente.

Este parámetro no tiene por qué tener significado especial más allá de "engañar" al navegador. El parámetro debe tener un valor aleatorio por cada vez que se usa porque el navegador, al ver esto, supone que eso influenciará el contenido del archivo que supuestamente procesa ese parámetro y por eso lo pide al servidor y no desde su caché. Pero ¿de dónde obtener un valor alearorio?: Pues de la fecha de la máquina cliente.

El código que implementa esto podría ser éste:


function LoadJS(nomarch) {
	var d = new Date(); // se crea un objeto tipo fecha
	var ele = document.getElementById(nomarch);
	if (ele == undefined) {
		var tagjs = document.createElement("script");
		tagjs.setAttribute("type", "text/javascript");
		tagjs.setAttribute("id", nomarch); 
		tagjs.setAttribute("src", "/js/"+nomarch+".js?rnd="+d.getTime()); // el parámetro "rnd" vale los milisegundos
		document.getElementsByTagName("head")[0].appendChild(tagjs);
	}
}

De esta forma se asegura que siempre se obtenga la versión "más nueva" del archivo a cargar dinámicamente.

Ajax que te quiero tanto y sin embargo...

Cuando un programador de entorno web conoce Ajax solo puede hacer una cosa, amarlo. Pero conforme la relación progresa uno se encuentra con ciertos inconvenientes, las cosas no son un camino de rosas. Tarde o temprano se encontrará con el siguiente problema:

Ha encontrado ese script que hace justo el efecto que uno quiere, lo ha probado estáticamente y funciona de maravillas. El script en cuestión es un poco pesado, la funcionalidad requiere que se le inicialice estáticamente pero esto no es un problema insalvable y además no todos los visitantes tendrán necesidad de usarlo. Entonces se decide que es mejor cargar todo con Ajax. Pero Ajax no ejecuta JavaScript y por lo tanto no hay manera de ejecutar el código de inicialización que el script requiere.

El código a continuación tiene como objetivo el simplemente didáctico, debido a que hay muchas implementaciones de Ajax, desde las más sencillas a las más elaboradas. Para este caso asumiré que existe un objeto JavaScript llamado Ajax y que implementa todas las características "sucias" de la técnica. Exporta al menos dos propiedades tipo función, una, a la que llamaré "response" será la que se ejecute cuando la petición finalice, y otra llamada "get" que será la que inicie la petición. Quienes estén familiarizados con Ajax entenderán esto.


var peticion = new Ajax();

function HacerPeticionAjax() {
	peticion.response = respuestaAjax;
	peticion.get("pagina.php");
}

function respuestaAjax(texto) {
	document.getElementById("contenedor").innerHTML = texto;
}

Primero se crea una instancia del objeto Ajax llamado "peticion". Luego en la función HacerPeticionAjax() se asigna a la propiedad "response" la función respuestaAjax; inmediatamente se ejecuta el método "get" pasando como parámetro el nombre del archivo que se quiere invocar. Como en la implementación del objeto la propiedad "response" requiere que la función acepte un parámetro se declara la función respuestaAjax con ese parámetro. Ese parámetro valdrá el contenido del archivo pedido, el cual es asignado al interior de un elemento HTML preparado para tal efecto.

Por supuesto, me he dejado muchas cosas afuera que cualquier implementación de Ajax tendría en cuenta (como el código de estado de la respuesta para saber si la petición fue exitosa o falló por algún motivo), pero para propósitos didácticos lo mantendré simple.

Ahora bien, usando esto como código base y sabiendo que además el contenido del archivo "pagina1.php" depende de código javaScript para funcionar correctamente, y usando lo visto hasta ahora sobre carga dinámica de código JavaScript, lo que hay que hacer es bastante evidente (en mi opinión):


var peticion = new Ajax();

function LoadJS(nomarch) {
	var d = new Date();
	var ele = document.getElementById(nomarch);
	if (ele == undefined) {
		var tagjs = document.createElement("script");
		tagjs.setAttribute("type", "text/javascript");
		tagjs.setAttribute("id", nomarch); 
		tagjs.setAttribute("src", "/js/"+nomarch+".js?rnd="+d.getTime());
		document.getElementsByTagName("head")[0].appendChild(tagjs);
	}
}

function HacerPeticionAjax() {
	LoadJS("miarchivojs3");
	peticion.response = respuestaAjax;
	peticion.get("pagina1.php");
}

function respuestaAjax(texto) {
	document.getElementById("contenedor").innerHTML = texto;
}

Simple y hermoso.

Lo que esperamos que suceda será que al ejecutar HacerPeticionAjax() se cargue dinámicamente el archivo "miarchivojs3.js", luego hacemos la petición Ajax la cual cargará el código HTML que depende del código JavaScript que está en "miarchivojs3.js", ¿cierto?.

Pues me temo que no, no necesariamente.

Estamos asumiendo que las cosas ocurrirán secuencialmente, es decir, primero sucede A y cuando ésto termine, sucederá B. Pero Internet no es un medio fiable y los navegadores tampoco se bloquean cuando hacen una petición al servidor.

Cuando se carga un archivo JavaScript de la forma que se hace en el código visto hasta ahora, el navegador seguirá ejecutando sus cosas mientras espera a que el servidor le sirva (valga la redundancia) el recurso (es decir, el archivo) solicitado.

Esto implica que para cuando se alcance el final de la función HacerPeticionAjax() puede o no (y lo más probable es que no) haberse cargado y ejecutado ya el código del archivo "miarchivojs3".

En esta página se puede ver el efecto descrito funcionando.

En este ejemplo el contenido relevante del HTML es este tag:


	<div id="contenedor"></div>

El archivo "pagina1.php" solamente contiene esto:


		<div id="dummy" style="color: red;"></div>

Para que, luego de ejecutarse la función respuestaAjax(texto), el DOM termine siendo esto:


	<div id="contenedor"><div id="dummy" style="color: red;"></div></div>

Mientras que el archivo "miarchivojs3.js" no puede ser más sencillo:


document.getElementById("dummy").innerHTML = "Encontré el elemento!";

Es decir, intenta poner una cadena de texto dentro del <div> "dummy". Pero como ese código se carga dinámicamente, es posible que se ejecute antes de que la petición Ajax termine, resultando en que no habrá ningún elemento HTML con id "dummy" todavía, y el navegador elevará una excepción. En mi caso Opera indica en la consola de errores lo siguiente:

Uncaught exception: TypeError: Cannot convert 'document.getElementById("dummy")' to object

Serio problema, ¿cierto?.

La solución es mover de lugar la llamada a la función LoadJS() para que se ejecute después de haber recibido la petición Ajax, es decir, ponerla al final de la función respuestaAjax(), esto funciona para la mayoría de los casos pero no para todos como se explica a continuación.

Como se ha comentado ya, el navegador ejecuta algunas cosas en paralelo, una de esas cosas es la construcción del DOM. Si nos referimos al código del ejemplo, se puede ver que la recepción de la petición Ajax se está agregando nuevo contenido al documento, si ese contenido es código HTML, el navegador debe parsearlo y por lo tanto construir nuevos nodos en el DOM, ese proceso se realiza en paralelo mientras se sigue ejecutando nuestra función. De esta forma, si el contenido que está en la variable "texto" es grande, es posible que el código JavaScript haga referenca a un elemento del DOM que todavía no ha sido construido por el navegador[5].

Aceptado. Esto es improbable que suceda puesto que el DOM a construir después de una petición Ajax debería ser enormemente grande para que demore más de lo que se tardaría hacer la petición al servidor, esperar por éste, obtener el código fuente del archivo .js y ejecutarlo. Pero es bueno tenerlo en cuenta si las cosas no marchan como se esperan.

Una posible solución a esto es darle tiempo al navegador para que construya el DOM no haciendo la llamada a LoadJS() hasta dejar pasar un tiempo definido, para eso se puede usar el método setTimeout(). A esto se le llama "ejecución diferida".

La solución.

A continuación el código de la solución al problema cargar dinámicamente código JavaScript luego de una petición Ajax.


var peticion = new Ajax();

function LoadJS(nomarch) {
	var d = new Date();
	var ele = document.getElementById(nomarch);
	if (ele == undefined) {
		var tagjs = document.createElement("script");
		tagjs.setAttribute("type", "text/javascript");
		tagjs.setAttribute("id", nomarch); 
		tagjs.setAttribute("src", "/js/"+nomarch+".js?rnd="+d.getTime());
		document.getElementsByTagName("head")[0].appendChild(tagjs);
	}
}

function HacerPeticionAjax() {
	peticion.response = respuestaAjax;
	peticion.get("pagina2.php");
}

function respuestaAjax(texto) {
	document.getElementById("contenedor").innerHTML = texto;
	LoadJS("miarchivojs3");
}

El mismo código funcionando puede ser probado aquí.

Preguntas y comentarios son bienvenidos. Hagan uso de los comentarios más abajo que, como ya habrán adivinado, es un caso práctico de lo que acabo de explicar.

Ejemplos del artículos.

Estos son enlaces a los ejemplos usandos en este artículo.

Notas.

1: Aunque JavaScript es el lenguaje más popular para hacer scripting en una página web, teóricamente con el tag <script> se pueden cargar otros lenguajes siempre y cuando el navegador pueda interpretarlo. Actualmente todos los navegadores modernos cuentan con un intérprete de JavaScript integrado en el propio navegador pero es posible que mediante plug-ins se puedan cargar intérpretes para otros lenguajes.

2: También es posible cargar un archivo desde un dominio completamente diferente indicando la URI completa.

3: Me temo que no todas las cadenas de caracteres que son nombres de archivos, serán "id"s válidos. Por ejemplo esta cadena de caracteres: "jquery.plugingenial.min" es un nombre de archivo válido pero no sirve como "id" ya que el carácter punto (.) no es un carácter válido para un "id". La solución consistiría en reemplazar los puntos por guión medio (o directamente eliminarlos de la cadena), pero la implementación de esta solución escapa al propósito de este artículo. Este tip puede ser adaptado para este propósito.

4: Sherlock Holmes dixit.

5: En computación esto se llama "el problema de los filósofos comensales". A una mesa se tienen sentados un número de filósofos que pueden estar en solo tres estados "pensando", "hablando" y "comiendo", cada plato de comida es finito. Como cada filósofo solo puede estar en uno de tres estados el problema consiste en mantener a los filósofos comiendo mientras tengan comida, hablando mientras tengan con quién, pensando mientras tengan a alguien que les hable pero no mucho tiempo pues se moriría de hambre.

Por Diego Romero,