jueves, 6 de octubre de 2011

Carga condicional de recursos en HTML

Yo no sé si a vosotros os pasa, pero a mi últimamente cada vez me pidan cosas más complicadas como si resultarán fáciles de desarrollar. En el fondo no tengo ninguna queja con ello, porque todo es producto de lo rápido que va la web y de la satisfactoria acogida que están teniendo en el mercado todo tipo de dispositivos móviles.

Hace mucho tiempo, estaba claro que un sitio web era sólo para IE, pero hoy en día todo ha cambiado. Hoy por hoy, mi sitio web debería visualizarse correctamente en todos los navegadores de escritorio (IE, Firefox, Chrome, Safari, Opera, etc.), también en dispositivos móviles (iPad, iPhone, Android, etc.) y además también podría conectarme desde la PlayStation 3, la XBOX 360 (que digo yo algún día le meterán un navegador), y ¿por qué no?, también desde el navegador que lleva integrado mi nevera (esto es coña, pero juraría que las he visto en el Corte Inglés).

Lo que quiero decir es que, “felizmente”, parece que tanto desarrolladores de web como los desarrolladores de navegadores, hemos entendido que el camino de los estándares es el único camino válido para una web universal y accesible por todos. He recalcado “felizmente” porque yo mismo soy tanto desarrollador web como usuario de múltiples dispositivos, y soy el primero que pone el grito en el cielo cuando me conecto a una web con Firefox y me dice que está optimizada para IE… o peor, simplemente no funciona con Firefox y por obligación tienes que utilizar IE.

En esta situación, como desarrolladores web podemos tomar algunas decisiones para intentar, en la medida de lo posible, que nuestras frágiles e insignificantes vidas sigan su curso natural sin perder la cabeza por el camino:

·         Utilizar un framework de javascript (leasé jQuery, Prototype, MooTools, Ext JS, etc.)

·         Utilizar una colección de controles de terceros para el lado del cliente (por ejemplo jQuery UI, jQuery Mobile, Ext JS, Dojo, etc.)

·         Utilizar una suite de controles de terceros para el lado del servidor (por ejemplo Telerik, Infragistics, Component One, DevExpress, etc.)

Todas estas recomendaciones persiguen un mismo propósito:
“Que mi código funcione y parezca el mismo, sin importar donde se esté ejecutando”.

Aunque los creadores de los navegadores siempre dicen que su producto cumple los estándares, lo cierto es que después cada navegador tiene sus peculiaridades y las defiende a capa y espada. Y es justamente para manejar estas diferencias, donde productos consolidados, bien testados y con el sello de “cross-browser” nos ayudan a no volvernos locos.

Por ejemplo, el caso más flagrante es el objeto event de Javascript.

Si no utilizamos un framework como jQuery, para capturar un clic sobre un botón deberíamos hacer lo siguiente:

<input type="button" value="Saludar" onclick="saludar(event);" />

 

<script type="text/javascript">

function saludar(e) {

var event = window.event || e;

if (window.event) { //ie

alert(event.srcElement.id);

}

else { //resto

alert(event.target.id);

}

}

</script>


Si te fijas, hemos tenido que incluir una instrucción “cross-browser” para capturar el objeto event. Si quieres más información sobre este tema, hace tiempo escribí un post sobre eventos cross-browser.

Ahora, y gracias a jQuery (aunque seguro que cualquier otro framework funciona de forma parecida), podemos escribir este otro código y olvidarnos de “esa diferencia” en el trato del objeto event que hacen distintos navegadores, puesto que jQuery se encarga de “normalizar” el objeto evento.

<input type="button" id="btnSaludar" value="Saludar" />

 

<script type="text/javascript">

$().ready(function (e) {

$("#btnSaludar").click(function (e) {

alert(e.target.id);

});

});

</script>


Una vez que te convencido de que utilices frameworks, y sobre todo en el lado del cliente, aún hay ciertas situaciones en las que es preciso diferenciar en qué navegador estamos para por ejemplo:

·         Cargar diferentes versiones de ficheros javascript (.js)

·         Cargar diferentes versiones de ficheros de hojas de estilos (.css)

En mi caso, poco a poco va apareciendo en escena el iPad y, aunque intentemos “escribir un solo código”, al final hay que terminar haciendo ajustes que sólo deben realizarse en función del tipo del navegador del cliente.

En aquí donde hablamos de “carga condicional de recursos”, esto es:
“Si eres un navegador de escritorio utiliza estos ficheros, pero si eres un iPad toma estos otros”.

Si queremos llevar a cabo la carga condicional de recursos desde el lado del servidor (desde ASP.NET), un ejemplo válido podría ser este (aunque seguro hay más y mejoras formas de implementar este concepto):

    Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load

        If Request.UserAgent.IndexOf("iPad") <> -1 Then

            AddStyleSheet(ResolveUrl("~/iPad.css"))

            AddScript(ResolveUrl("~/iPad.js"))

        Else

            AddStyleSheet(ResolveUrl("~/DesktopBrowser.css"))

            AddScript(ResolveUrl("~/DesktopBrowser.js"))

        End If

    End Sub

 

    Private Sub AddStyleSheet(ByVal url As String)

        Dim styleSheet As New HtmlLink()

        styleSheet.Href = url

        styleSheet.Attributes.Add("rel", "stylesheet")

        styleSheet.Attributes.Add("type", "text/css")

        styleSheet.Attributes.Add("media", "all")

        Page.Header.Controls.Add(styleSheet)

    End Sub

 

    Private Sub AddScript(ByVal url As String)

        Dim key As String = IO.Path.GetFileName(url)

        ClientScript.RegisterClientScriptInclude(key, url)

    End Sub


La función AddStyleSheet agregará a nuestro documento la siguiente línea:

<link href="/ DesktopBrowser.css" rel="stylesheet" type="text/css" media="all" />

Mientrás que AddScript agregará este otra línea:

<script src="/ DesktopBrowser.js" type="text/javascript"></script>


Para AddScript (y googlenado un poco), he encontrado otras formas de implementar la misma funcionalidad (que también servirían para AddStyleSheet):

    Private Sub AddScript(ByVal url As String)

        Dim script As LiteralControl = New LiteralControl()

        script.Text = String.Format("<script type=""text/javascript"" src=""{0}""></script>", url)

        Page.Header.Controls.Add(script)

    End Sub


    Private Sub AddScript(ByVal url As String)

        Dim tag As String = "script"

        Dim script As New HtmlGenericControl(tag)

        script.Attributes.Add("type", "text/javascript")

        script.Attributes.Add("src", url)

        Page.Header.Controls.Add(script)

    End Sub


En cualquiera de los casos, la solución está clara:

·         Detectar el navegador en el código del servidor.

·         Registrar los ficheros necesarios de forma condicional.

Si por el contrario queremos realizar esta misma operación en el lado del cliente, una posible soluciones sería crear un elemento HTML al vuelo (link o script) e insertarlo en el árbol del documento.

Ya en su momento, escribí un post sobre crear nuevos elementos HTML a través de jQuery que explicaba las distintas opciones que se me ocurrían para crear nuevos elementos HTML al vuelo.

Si aplicamos lo visto en ese post a los requerimientos de este post (link o script), el código quedaría como sigue:

        $().ready(function (e) {

            if (navigator.userAgent.indexOf("iPad") != -1) {

                addStyleSheet("/WebSite1/iPad.css");

                addScript("/WebSite1/iPad.js");

            }

            else {

                addStyleSheet("/WebSite1/DesktopBrowser.css");

                addScript("/WebSite1/DesktopBrowser.js");

            }

        });

 

        function addStyleSheet(url) {

            var styleSheet = $("<link>", {

                href: url,

                rel: "stylesheet",

                type: "text/css",

                media: "all"

            });

            $("head").append(styleSheet);

        }

 

        function addScript(url) {

            var script = $("<script>", {

                src: url,

                type: "text/javascript"

            });

            $("head").append(script);

        }

 

Si te fijas, la solución en el cliente es casi idéntica a la solución del servidor. Las únicas diferencias es cómo crear el elemento y como “inyectarlo” en nuestro documento.

Aunque esta solución es válida (de hecho, es muy válida), existen formas en javascript más elegantes de cargar recursos bajo demanda. Por ejemplo, el mismo jQuery nos ofrece de serie el método getScript que carga el fichero .js solicitado y además nos ofrece una función de tipo callback que se ejecutará cuando se haya descargado con éxito el fichero (esto es una mejora frente el método anterior de carga). Veamos un ejemplo:

        function addScript(url) {

            $.getScript(url, function (data, textStatus) {

                //data es el texto del fichero .js

                //textStatus es "success"

            });

        }


Si comparamos ambos métodos, podemos llegar a las siguientes conclusiones:

·         El método que crea el elemento <script> al vuelo, primero crea el elemento y después lo anexa al documento, lo que provoca automáticamente la descarga del recurso solicitado.

·         El método getScript, primero descarga el fichero a través de AJAX y después lo ejecuta. Esto significa que no agrega ningún elemento <script> al documento, sino que simplemente su ejecución lo hace disponible al propio documento, y además tenemos una función de callback para poder hacer algo después de la ejecución del código solicitado.

En mi opinión, el método getScript nos da más juego y además cumple con mi obsesión de pasar por el aro de jQuery.

La pregunta que me surge ahora es si puedo aprovechar AJAX para cargar un fichero .css y así aprovechar el callback. Y por supuesto, la respuesta es SÍ, sólo que ahora y después de descargar el fichero .css, crearemos un elemento <style> para volcar el contenido devuelto por el servidor:

        function addStyleSheet(url) {

            $.ajax({

                url: url,

                success: function (data, textStatus, jqXHR) {

                    $("<style>", { html: data }).appendTo("head");

                    //do something...

                }

            });

        }


Llegados a este punto, todo parece indicar que ya tenemos distintas formas válidas de cargar recursos bajo demanda, pero la realidad es que quería llegar hasta aquí para ahora explorar soluciones totalmente enfocadas a la carga de recursos condicionales.

Estoy hablando de yepnope.js.

Al respecto de yepnope.js, te diré que en la página de etnassoft hay un artículo buenísimo que analiza la librería y que no te puedes perder.

A mí me gusta porque:

·         Es sencilla.

·         Es asíncrona y descarga los recursos en el orden establecido.

·         Implementa callbacks.

·         Y sobre todo se integra perfectamente con Modernizr. Una librería que se integra de serie en nuevos proyectos de ASP.NET MVC y que sin duda ya es parte de mi toolbelt de javascript, junto a jQuery.

Lo primero es descargar el fichero y ahí va otra buena noticia: está disponible a través de NuGet, así que su descarga es muy sencilla.

Una vez descargado, veamos cómo queda nuestro anterior código con yepnope.

        yepnope({

            test: navigator.userAgent.indexOf("iPad") != -1,

            yep: ["/WebSite1/iPad.css", "/WebSite1/iPad.js"],

            nope: ["/WebSite1/DesktopBrowser.css", "/WebSite1/DesktopBrowser.js"],

            callback: function (url, result, key) {

                //do something...

            }

        });

 

Los parámetros de la función son:

·         test. Condición a evaluar.

·         yep. Array de recursos a cargar si test devuelve true.

·         nope. Array de recursos a cargar si test devuelve false.

·         callback. Función de retrollamada.

No negarás que es un código compacto, sencillo y elegante.

Me gusta yepnope!

Un saludo!

No hay comentarios:

Publicar un comentario