Mostrando entradas con la etiqueta Javascript. Mostrar todas las entradas
Mostrando entradas con la etiqueta Javascript. Mostrar todas las entradas

domingo, 13 de octubre de 2013

Carga condicional de recursos con yepnope en ASP.NET MVC

En la empresa estamos desarrollando una aplicación web en ASP.NET MVC para la que queremos dar cierto soporte a dispositivos móviles. Digo lo de “cierto” porque nuestra intención es que el mismo front-end sirva para todos los dispositivos por igual (ya sean desktop o mobile). Sin embargo, hay ocasiones donde en función del tipo de dispositivo nos gustaría cargar uno u otro recurso (ya sea un .js o un .css).

La solución por la que hemos optado ha sido la carga condicional de recursos a través de yepnope. En este blog ya se escribió sobre yepnope y la verdad es que resulta muy sencillo de utilizar.

Por otro lado, todos usamos Modernizr para detectar características del navegador. La idea es detectar si el navegador es táctil o no y en función de esto cargar una u otra hoja de estilos.

El propio Modernizr suministra un cargador de recursos basado en yepnope a través del método Moderniz.load. Por defecto, el paquete de Nuget de Moderniz que se incluye en las plantillas de los proyectos de MVC no incluye esta característica. Un simple vistazo a la cabecera del fichero .js de Modernizr nos da la clave del asunto:

*
* Modernizr has an optional (not included) conditional resource loader
* called Modernizr.load(), based on Yepnope.js (yepnopejs.com).
* To get a build that includes Modernizr.load(), as well as choosing
* which tests to include, go to www.modernizr.com/download/
*

Por otro lado, sí que hay un paquete de Nuget que incluye el cargador de recursos, el paquete se llama Modernizr.full. Entiendo que este paquete sería lo mismo que descargar Modernizr desde la web e incluir todas opciones disponibles (el método Modernizr.load está marcado como predeterminado en la sección “Extra”).

En cualquier caso, nuestra decisión ha sido utilizar yepnope directamente sin pasar por Modernizr. Me gustaría pensar que puedo actualizar tanto Modernizr como yeopnope sin tener que preocuparme de si uno afecta a otro o viceversa.

En esta situación, la carga condicional del fichero .css sería como sigue:

<script>
    yepnope({
        test: Modernizr.touch,
        yep: '@Url.Content("~/Content/mobile.css")',
        nope: '@Url.Content("~/Content/desktop.css")'
    });
</script>


Esto ya funciona y no hay ningún problema. Sin embargo no estamos utilizando bundles y para mí son casi obligados simplemente por el hecho de no tener que preocuparme de si está o no cacheado en cliente el recurso ya que cualquier cambio en el servidor provocará que el cliente refresque el recurso automáticamente.


Siendo así, mismo ejemplo pero ahora con bundles:



<script>
    yepnope({
        test: Modernizr.touch,
        yep: '@Styles.Url("~/Content/mobile")',
        nope: '@Styles.Url("~/Content/desktop")'
    });
</script>


Simplemente hemos cambiado Url.Content por Styles.Url, pero obtenemos el siguiente error de javascript:


image


El problema está en que yepnope trata a nuestro fichero .css descargado como si fuera un fichero .js. Esto lo hace porque el recurso no acaba con la extension .css y entonces no sabe que lo que estamos cargando es una hoja de estilos en vez de un fichero de script. Fíjate que el código javascript que estamos cargando finalmente en cliente es el siguiente:


<script>
    yepnope({
        test: Modernizr.touch,
        yep: '/Content/mobile?v=lZic6x4eHbbXGYLQzArfcY-IMpq4H2ZVRQXn0B1-3Bc1',
        nope: '/Content/desktop?v=BvoYWkJHqnyVrPiZcSn963sWrvuVmBvTQ4y7_w9WabE1'
    });
</script>


¿Cómo decirle entonces a yepnope que trate el recurso como css en vez de como js? Pues a través del prefijo !css.Según la documentación de yepnope es justo para casos como en el que estamos:


The css! prefix is for those people who need to load css files without a `.css` extension. Since yepnope 1.5 - yepnope can detect the presence of query parameters without the help of the css prefix, so this is usually for cases when the css files have a php ending or something similar.


Este prefijo no viene de serie con yepnope y hay que descargarlo desde aquí https://github.com/SlexAxton/yepnope.js/blob/master/prefixes/yepnope.css-prefix.js


Una vez descargado, lo utilizaremos y ahora sí funcionará yepnope junto a nuestros bundles:


<script>
    yepnope({
        test: Modernizr.touch,
        yep: 'css!@Styles.Url("~/Content/mobile")',
        nope: 'css!@Styles.Url("~/Content/desktop")'
    });
</script>


Cierto es que se podría haber evitado la carga condicional de recursos en este caso concreto utilizando las las clases de Modernizr .touch y .no-touch que agrega automáticamente al elemento html. Es decir, lo siguiente es válido y no requiere nada de código:


<style>
    .touch body {
        color: red;
    }
    .no-touch body {
        color: yellow;
    }
</style>


Sin embargo sigo pensando que depende de en que escenarios prefiero la carga condicional de recursos.


Un saludo!

miércoles, 10 de octubre de 2012

Javascript: The Good Parts y JSLint

Recientemente parece que el libro Javascript: The Good Parts me persigue allá donde vaya.

Hace poco lo conocí a través de twitter, pero justo hoy, en el evento html tour de Madrid de Plain Concepts, se ha vuelto a recomendar la lectura de este libro.

Lo cierto es que cuando lo tuve en mi poder lo devoré en un solo fin de semana. Tampoco es ningún mérito, el libro es pequeño y no se anda con rodeos.

Después de su lectura, una sola idea ronda por mi cabeza “Javascript es un lenguaje incomprendido”.  Yo al menos no tengo ningún reparo en reconocer que, antes de la lectura de este libro, mi opinión sobre Javascript distaba de ser algo positivo. Además, también distaba de tener el suficiente conocimiento como para juzgarlo con conocimiento de causa.

Hoy durante el html tour, he oído una de las mejores frases que uno podría oír sobre Javascript “Javascript: the worst part. El principal problema es nuestra ignorancia”.

Creo que realmente el problema de Javascript es que cualquiera puede programar en Javascript. Da igual si eres un consumado experto o un tierno novato, escribe cuatro líneas de código y Javascript intentará por todos los medios ocultar tus vicios y ejecutar tu código.

Javascript es algo más que la función alert y la función confirm. Es un lenguaje de programación en toda regla pero con un pésimo interprete que, por no romper la web, no se quejará casi nunca y evitará así que aprendas de tus errores.

Además tampoco esperes encontrar en Javascript un lenguaje POO al estilo de C#. Javascript tiene su propia identidad y su forma singular de hacer las cosas, te gustará o no pero es lo que hay, es mejor que no intentes cambiar a Javascript sino cambiar tu forma de relacionarte con Javascript. Como dice Bruce Lee, “Be water my friend”.

Como lo cierto es que sí queremos aprender de nuestros errores y queremos ser mejores programadores, en mi opinión es indispensable realizar las siguientes tareas:

  • Leer Javascript: The Good Parts.
  • Utilizar JSLint para que valide nuestro código Javascript.

Como leerte el libro mientras te duermes plácidamente me parece un exceso, en este post lo que voy a intentar es explicarte brevemente cómo instalar JSLint y como usarlo y configurarlo.

JSLint es una herramienta creada por el mismo autor del libro, Douglas Crockford. El sitio web de la herramienta es http://www.jslint.com/. Según sus propias palabras, JSLint es una herramienta de calidad de código, es decir, una herramienta que busca problemas en tú código Javascript.

La mayoría de los problemas son bien conocidos y no admiten debate alguno, otros son guías de estilo propuestos por el autor, que podrías ignorar si así lo crees oportuno.

Aunque puedes utilizar un validador en línea desde su página, hay un proyecto en CodePlex que integra la herramienta en Visual Studio http://jslint4vs2010.codeplex.com/.

Para instalarlo, puedes descargar la extensión para Visual Studio 2010 http://visualstudiogallery.msdn.microsoft.com/961e6734-cd3a-4afb-a121-4541742b912e

También está disponible para su descarga desde el administrador de extensiones

clip_image002

Una vez instalado, tenemos disponible una nueva opción de menú: Herramientas > JS Lint Options…

clip_image004

 clip_image006

El uso de JSLint es bastante sencillo, por defecto hará lo siguiente.

  • Mostrará los errores como “warnings”
  • Sólo validará los ficheros .js
  • Ejecutará la herramienta en cada Build
  • Cancelará el Build si encuentra algún error

Como vemos en las pantallas anteriores, podemos configurar multitud de aspectos de la herramienta: desde validar ficheros .html, no cancelar el Build, ejecutarlo cuando guardamos el fichero, excluir ciertas secciones de nuestro código a través de comentarios, etc.

Mi recomendación (copiada vilmente del ponente del html tour) es que utilicemos JSLint para cualquier nuevo proyecto y que, para proyectos existentes, hagamos lo que podamos. Es decir, hoy mismo me decía un compañero que lo había activado para un proyecto en mantenimiento y el número de errores había sido demasiado alto… Yo directamente en mis proyectos antiguos no lo activaré porque tengo amor propio y no me apetece que JSLint me saque los colores cuando analice mi código antiguo.

Para ver a JSLint en acción, crearemos un nuevo Sitio Web y agregaremos un fichero .js en el que escribiremos el siguiente código:

function foo() {

    bar = "baz";

    return bar;

}

for (var i = 0; i < 5; i++) {

    alert(foo());

}

 

Que nos devolverá los siguientes warnings:

clip_image008

La compilación ha sido cancelada porque JSLint encontró errores de validación.

  • Estamos utilizando la variable “bar” pero no la hemos definido previamente (hemos olvidado var y eso implica que la variable bar se está creando en el contexto global y no el ámbito privado de la función foo).
  • En Javascript no existe el ámbito de bloque, luego “for (var i” podría llevarnos a engaño. Después de la instrucción for, “i” vale 5 porque “i” sigue existiendo.

Vamos a intentar solucionar los errores:

function foo() {

    var bar = "baz"; // hemos agregado var

    return bar;

}

var i = 0; // hemos declarado la variable fuera del for

for (i; i < 5; i++) {

    alert(foo());

}

 

clip_image010

Pues lo hemos logrado a medias, ahora nos informa que “no esperaba ++” y que “alert” ha sido utilizada antes de ser definida.

En cuanto al operador ++, según Douglas Crockford, no le gusta. Yo no voy a discutir a este señor (no podría), pero como ahora mismo no sé que espera encontrar y además con este excusa podemos ver como “desactivar” ciertas validaciones de JSLint en la pantalla JSLint Options.

clip_image012

Ahora ya no tenemos errores de JSLint:

clip_image014

Respecto a los errores relacionados con las guías de estilo, bastaría con quitar un espacio entre “var bar y =”

function foo() {

    var bar= "baz";

    return bar;

}

 

Ahora JSLint nos informará sobre este nuevo error relacionado con el estilo:

clip_image016

Para los que trabajamos con Visual Studio, muchos de estos problemas se solucionan simplemente con el atajo Ctrl + K, Ctrl + D (Menú Editar > Avanzadas > Dar formato al documento). De hecho, yo también utilizo este comando para ver si mi código tiene algún error, si Visual Studio no es capaz de dar formato al documento Javascript… algo no va bien.

Otros comandos que agrega JSLint a Visual Studio son nuevas opciones en el menú contextual desde el explorador de soluciones (Solution Explorer).

clip_image017

La opción “Skip on build (item)” cobra especialmente importancia si trabajamos con librerías de terceros. Por ejemplo, jQuery 1.8.2 lanza 455 advertencias en JSLint! De este modo o lo excluimos o estamos en un serio aprieto.

Si estás trabajando con VS2012 e instalas la extensión WebEssentials (un must-have en toda regla) http://vswebessentials.com/ automáticamente dispondrás de JSHint (ver que ahora hemos cambiado la L por una H) y podrás configurar todo lo relacionado con la herramienta desde Herramientas > Opciones > Web Essentials > JSHint

image

Las diferencias entre L y H (JSLint y JSHint) radican en que JSHint es un fork de JSLint. Es decir, ambos están vigentes y ambos son válidos, utilizar uno u otro es cuestión de gustos. El porqué aquí http://anton.kovalyov.net/2011/02/20/why-i-forked-jslint-to-jshint/ Por supuesto JSHint también tiene su propio validador on-line http://www.jshint.com/

Espero que este post te sirva para entender que Javascript es un lenguaje que merece toda tu atención y que hay herramientas que podrían ayudarnos a escribir mejor código.

Un saludo!

 

 

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!