Comunidad de diseño web y desarrollo en internet online

Cargar imágenes antes de mostrar contenido desde AJAX

Un usuario escribió un problema en el foro de Cristalab donde mencionaba que al cargar asincrónicamente HTML mediante AJAX que contiene imágenes al mostrarlo éstas parecieran no haber cargando aún. Este comportamiento es más que normal teniendo en cuenta la naturaleza de las respuestas HTTP. Me explico, cuando ingresamos una dirección en nuestro navegador, éste realiza una petición a dicho servidor quien devuelve únicamente el código HTML de la página solicitada. Esa se cuenta cómo una petición. Ahora, si el HTML contiene etiquetas de imagen, de script u hojas de estilo; el navegador realiza una petición por cada uno de los recursos que el HTML necesita.

Podemos deducir que las peticiones AJAX siendo peticiones HTTP solicitan una única respuesta del servidor, así que cuando llegan de nuevo al navegador lo único que tendremos en nuestro poder sería ese recurso (sea HTML, JSON, XML, etc) que debido a las restricciones de seguridad actuales sólo puede ser texto. Así, al insertar el código recibido en nuestra página el navegador intentará cargar los recursos solicitados por el código que insertamos tal como lo vimos en el párrafo anterior. Lo que vamos a ver ahora es una posible solución para quienes necesiten mostrar el contenido sólo hasta que todas las imágenes estén cargadas.

Para empezar debemos capturar entonces todas las imágenes que vienen en el HTML, esto lo podemos hacer convirtiéndolo en una estructura DOM válida y así hacer uso de los métodos que nos provee Javascript. Para convertir código HTML en una estructura DOM debemos inyectarlo en un nodo DOM válido:

Código :

var html = '<img src="imagen.jpg" />';
var tmpContainer = document.createElement('div');
tmpContainer.innerHTML = html;


De esta manera, nuestro HTML ya puede ser manipulado con los métodos getElementById y getElementsByTagName. En este caso use un <div /> que es un contenedor genérico.

Código :

var imgNodes = tmpContainer.getElementsByTagName('img');


Ya con las imágenes capturadas vamos a la parte donde hacemos la precarga. Para esto vamos a usar un antiguo constructor llamado Image. Este constructor no hace parte del estándar DOM pero nos permite crear elementos de imágenes que se comportan como tal sin que hagan parte del documento, es decir, le puedes asignar atributos y cuando se crean se realiza una petición HTTP para intentar cargar dicha imagen. Cabe aclarar que el hecho de que no haga parte del estándar no quiere decir que no sea crossbrowser. Como ya lo mencioné, es un constructor muy antiguo y funciona en todos los navegadores.

Código :

var imgs = [];

for(var i = 0, l = imgNodes.length; i < l; i++){
   imgs[i] = new Image();
   imgs[i].src = imgNodes[i].getAttribute('src');
}


Lo que hacemos es recorrer las imágenes de nuestra nueva estructura DOM y crear un objeto por cada una de ellas y almacenarlos dentro en un arreglo. Nótese cómo igualo el atributo src de cada objeto creado con el de nuestras imágenes. En ese momento sería en el que virtualmente se hace la petición HTTP para intentar cargar la imagen.

Pero bien, ¿cómo sabemos si la imagen ya fue cargada?, para esto podemos usar el evento load de cada imagen.

Las imágenes pueden disparar 2 tipos de eventos de carga: load (la imagen fue cargada exitosamente) y error (ocurrió un error cargando la imagen, esto incluye errores HTTP comunes como el 404, 500, etc). Usando un simple escucha podemos verificar la carga de la imagen.

Código :

img.onload = function(){
     console.log('La imagen ya cargó');
};

img.onerror = function(){
     console.log('Ocurrió un error al cargar la imagen');
};


Utilizando esta característica a nuestro favor vamos a hacer un conteo de las imágenes que se han cargado correctamente y las que no para así saber en qué momento mostrar el contenido. Es necesario también verificar las que arrojan error porque de otro modo nunca sabremos si el servidor ya respondió.

Código :

var imgs = [];
var counter = 0;
var limit = imgNodes.length;
var incrFn = function(){
   counter++;
   
   if(counter >= limit){
      callback();
      tmpContainer = null;
   }
};

for(var i = 0; i < limit; i++){
   imgs[i] = new Image();
   imgs[i].src = imgNodes[i].getAttribute('src');
   imgs[i].onload = incrFn;
   imgs[i].onerror = incrFn;
}


Para hacerlo un poco más portable podemos crear una función y así utilizarlo en varias aplicaciones. Veamos el código completo para el problema de nuestro amigo en el foro.

Para este código HTML:

Código :

<div id="content"></div>


Código :

var ajax = new XMLHttpRequest();
ajax.open('GET', 'content.html', true);
ajax.onreadystatechange = function(){
   if(ajax.readyState === 4){
      var responseText = ajax.responseText;
      preloadHTMLImages(responseText, function(){
         document.getElementById('content').innerHTML = responseText;
      });
   }
};

ajax.send();
   
document.getElementById('content').innerHTML = "Cargando imágenes...";


y el código de nuestra función:

Código :

function preloadHTMLImages(html, callback){   
   var tmpContainer = document.createElement('div');
   tmpContainer.innerHTML = html;
   
   var imgNodes = tmpContainer.getElementsByTagName('img');
   var imgs = []; //en este arreglo almacenamos los objetos Image
   var counter = 0; //el contador que se encarga de avisarnos
   var limit = imgNodes.length;
       
   //esta es la funcion que se encarga de chequear e incrementar el contador
   var incrFn = function(){ 
      counter++;
      
      if(counter >= limit){
         callback();
         tmpContainer = null;
      }
   };
   
   for(var i = 0; i < limit; i++){
      imgs[i] = new Image();
      imgs[i].src = imgNodes[i].getAttribute('src');
      imgs[i].onload = incrFn;
      imgs[i].onerror = incrFn;
   }
}


Como pueden ver, creé el objeto XHR usando el constructor XMLHttpRequest que es soportado desde Internet Explorer 7. Si requieren el objeto para un navegador anterior pueden ver cómo hacerlo en otro tutorial. Además, usé un callback para mostrar el contenido después de que las imágenes ya estén cargadas.

Esta podría ser una de las tantas soluciones que existen. También podríamos mediante expresiones regulares capturar las URL de las imágenes y precargarlas mediante el constructor Image pero considero que crear estructuras DOM es algo más práctico.

Pueden ver el código funcionando aquí.

Éxitos.

¿Sabes SQL? ¿No-SQL? Aprende MySQL, PostgreSQL, MongoDB, Redis y más con el Curso Profesional de Bases de Datos que empieza el martes, en vivo.

Publica tu comentario

o puedes...

¿Estás registrado en Cristalab y quieres
publicar tu URL y avatar?

¿No estás registrado aún pero quieres hacerlo antes de publicar tu comentario?

Registrate