Overflow My Brain

/* ASP.NET, HTML5, CSS, JavaScript, Windows y más... */

Cuidado con el nuevo ScriptResourceDefinition y LoadScriptsBeforeUI

Una de las cosas que personalmente más me gustan de la nueva versión de .NET 4.5 es el soporte para definiciones de scripts de cliente desde cnds, local, con versiones debug, esto nos facilita enormemente la tarea de cambiar a mano las referencias de los scripts en las páginas por sus versiones minificadas y de implementar los mecanismos de testeo de carga de escript desde los cnd.

Bien dejaré para otro post, que tengo en borrador, las posibles maneras de implementar los mecanismos de carga de scripts apoyándonos en proveedores de CDN tanto a mano como utilizando librerías de terceros.

Para este post, lo que se necesita tener claro, es que una red de entrega de contenidos (CDN) no es más que un servidor conectado a una red con un gran ancho de banda y que nuestro DNS resolverá, de manera transparente para nosotros, hacia el más próximo geográficamente, las ventajas de los CDN las comentaré en ese otro post prometido.

Dado que el "servidor"  que nos va a entregar los recursos que nuestro sitio necesite está fuera de nuestro control por decirlo de algún modo, tenemos que ser capaces de determinar si el recurso se ha entregado correctamente, dado que la red de CDN puede estar caída, el cliente no tiene acceso a ese servidor por alguna regla de firewall, nuestra aplicación ha sido desplegada dentro de una intranet sin conexión al exterior, etc. En ese caso, implementamos un mecanismo que chequea la correcta descarga del recurso, y en caso de haber fallado obtenemos una copia de el mismo recurso desde otro lugar, normalmente desde el mismo sitio (servidor) que está entregando el resto de la aplicación.

Bien como decía al principio del post, todo este mecanismo de entrega de recursos y chequeo lo han implementado los chicos de Microsoft en las últimas versiones .NET de forma que es casi transparente para el desarrollador, veamos un ejemplo:

Lo primero que debemos hacer es crear la definición de nuestros scripts y registrarlos en el ScriptManager de nuestra aplicación, lo haremos en el aplicationstart de nuestro global.asax. Para este ejemplo voy a referenciar la jQuery 1.7.1 que es la que viene con las plantillas de Visual Studio y  utilizando los CDN de Microsoft 

 Sub ApplicationStart(ByVal sender As Object, ByVal e As EventArgs)
        ScriptManager.ScriptResourceMapping.AddDefinition("jquery", _
             New ScriptResourceDefinition() With {
                .CdnDebugPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.js",
                .CdnPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js",
                .CdnSupportsSecureConnection = True,
                .DebugPath = "~/jquery-1.7.1.js",
                .Path = "~/jquery-1.7.1.min.js",
                .LoadSuccessExpression = "typeof(window.jQuery) !== 'undefined' "
              })

End Sub</pre></div>

Como podemos ver en el código anterior se han definido una ruta para descargar la librería de jQuery desde el CND de Microsoft y en cas de que esto fallase se obtendría desde el directorio raíz de nuestro sitio. Como supongo que ya os habréis dado cuenta, las propiedades CndDebugPath y DebugPath son utilizadas cuando nuestra aplicación está en modo debug para no tener que trabajar con las versiones modificadas y la entrada de CDNSupportSecureConnection se utiliza si nuestro sitio se despliega tras una puerta SSL el ScriptManager se encargará de cambiar el esquema de la URL por nosotros, evidentemente el CDN que elijamos debe soportar conexiones seguras

Y ahora viene lo interesante, LoadSuccessExpression. Esta propiedad nos permite indicar una expresión booleana que nos ayudará a determinar si el recurso se ha cargado correctamente desde el CDN o bien necesitamos descargarlo desde la ubicación por defecto indicada en la propiedad .Path.

Veamos como funciona, para ello añadimos una página a nuestro proyecto y en ella ubicamos un ScriptManager y añadimos en él la referencia al script que hemos definido anteriormente.

<asp:ScriptManager ID="SM" runat="server" EnableCdn="True"  EnableCdnFallback="True">
                <Scripts>
                    <asp:ScriptReference Name="jquery" />
                </Scripts>
</asp:ScriptManager>

Como podemos ver le hemos indicado a nuestro ScriptManager que utilice el CDN para descargar los scripts(EnableCdn=True) y también le hemos indicado que compruebe que se ha obtenido correctamente el recurso (EnableCdnFallBack=True) Y con esto ya podríamos referenciar nuestros scripts. Un detalle a tener en cuenta, es que si utilizamos CompositeScript en el ScriptManager, este evidentemente obviará las entradas de CDN y ni siquiera generará el código de comprobación. Veamos que es lo que hace:

Si ejecutamos la página con esta configuración y nos fijamos en el timeline de nuestro navegador veremos algo como lo siguiente:

image

Como podemos apreciar se ha descargado jQuery desde el CDN de Microsoft y la página a la que estamos accediendo. Ahora vamos a simular un falla en el CDN para ello nos desconectamos de internet modificamos la ruta al recurso externo en la definición que hicimos al principio en el global.asax, yo para el caso voy a añadir unas cuantas "a" al principio de la ruta de modo que cuando nos pida el recurso obtenga un precioso error 404. Vamos que pasa ahora en nuestro timeline:

image

¿Que ha pasado? Pues muy sencillo nuestro navegador ha intentado descargar jQuery desde una ruta externa(el CDN configurado) y como no lo ha podido recuperar pues ha solicitado el recurso en la ruta alternativa, la local del sitio. Pero, ¿como consigue estoy ASP.NET de forma tan transparente y sencilla? veamos el código que ha generado el ScriptManager en nuestra página.

<body>
    <form method="post" action="Default.aspx" id="form1">
<div class="aspNetHidden">
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="zxsHSHmnqdDn7JVoqj31qDwGt1SRobbt6CH6LPkQJR6ltz/aF/6oVTsuiZVxHzJr+bZjb3zPFnVJaIRLSNq815v7DSiN6ipwdNSKg1w4sWg=" />
</div>


<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.js" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
(typeof(window.jQuery) !== 'undefined' )||document.write('<script type="text/javascript" src="jquery-1.7.1.js"><\/script>');//]]>
</script>


    <div>
    Aquí iría el contenido
    </div>
    </form>
</body>
</html>

Como podemos ver, lo primero que hace es tratar de obtener el script desde la ruta que le hemos indicado como externa, luego comprueba que esto ha sido correcto y en caso contrario referencia el script desde la raíz del sitio.

Sencillo verdad?, entonces, ¿cual es el problema? Veámoslo.

EL PROBLEMA

Una de las cosas de las que adolecía el ScriptManager era que al ser necesario ubicarlo dentro de las etiquetas <form> si utilizabas la característica de CompositeScript o simplemente referenciabas tus scripts como hemos hecho en el caso anterior, estos serían referenciados dentro del form, por el medio de la página, no te permitía meterlos dentro de las etiquetas <head> ni al final de la página como recomiendan los guías de buenas prácticas y rendimiento lo cual era un problema ya no sólo de elegancia de tu código sino que muchas veces tus scripts hacían referencia a elementos del DOM que todavía no habían sido cargados y es cuando necesitábamos hacer uso del famoso $(document).ready() u otros mecanismos.

Vale pero, ¿por qué lo estás contando en pasado René? pues bien, con la aparición de la versión 4 de .NET Framework incluyeron una propiedad LoadScriptsBeforeUI que nos permitía controlar precisamente si los scrips se cargaban antes o después del DOM del form, pues bien paradójicamente este es el problema. Vamos a modificar nuestro ScriptManager para ver un ejemplo.

  <asp:ScriptManager ID="SM" runat="server" EnableCdn="True"  EnableCdnFallback="True" LoadScriptsBeforeUI="False">
                <Scripts>
                    <asp:ScriptReference Name="jquery" />
                </Scripts>
 </asp:ScriptManager>

Si observamos, tan sólo hemos añadido una propiedad (LoadScriptsBeforeUI=False) para que cargue los scripts al final de la página. Ejecutamos, miramos el timeline….

image

¿Que ha pasado? ¿porque se descarga el mismo recurso dos veces de ambos sitios? La respuesta al porque es sencilla, el resto…. Si miramos nuevamente el código generado en nuestra página por el ScriptManager vemos:

<body>
    <form method="post" action="Default.aspx" id="form1">
<div class="aspNetHidden">
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="lKhvVLV6li7WKZH33oqGI+FLFQkxa0qGpRPAWcPC2fQNtp9X7GLHN0k5WkDsEZduaUSia6H41JxhyMjy45iaa80InJmx4/B7XaT7QyOO0Gg=" />
</div>


<script type="text/javascript">
//<![CDATA[
(typeof(window.jQuery) !== 'undefined' )||document.write('<script type="text/javascript" src="jquery-1.7.1.js"><\/script>');//]]>
</script>


    <div>
    Aquí iría el contenido
    </div>


<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.js" type="text/javascript"></script></form>
</body>
</html>

Como podemos ver en el código, al indicarle al ScriptManager que me llevase el código de script al final de la página me ha llevado sólo la referencia al recurso, y no la expresión para comprobar el alive del CDN, de este modo lo que ocurre es que cuando comprueba si jQuery se ha cargado correctamente como no se ha pedido el recurso, evidentemente falla y se carga desde la ubicación local. el navegador seguirá procesando el DOM de la página, y al llegar al final nuevamente la jQuery pero esta vez al CDN externo

Siempre que se trabaje con recursos externos, o no tan externos, es recomendable vigilar el timeline de nuestras aplicaciones, como vimos en este caso, si no tenemos cuidado al configurar el ScriptManager, estaríamos perdiendo todas las ventajas de utilizar un CDN, más bien lo estaríamos convirtiendo en un problema.

Este comportamiento tan extraño lo he reportado en Microsoft Conect, aquí, así si se os ocurre una solución o alguna aportación al problema, la incidencia ha sido abierta como pública así que  no dudéis en comentarlo

 

Nos leemos, René Pacios

Sitio Web de Ejemplo: Descargar

Referencias:

Acerca de René

René Pacios es un apasionado de la tecnología, autodidacta, emprendedor, le encanta el desarrollo web, para moviles, aplicaciones, todo aquello que automatice tareas y haga que las máquinas trabajen para él. Es un gran fan de las tecnologías Microsoft, y le encanta estar a la última siempre que el tiempo se lo permite. Siempre quiso ser cantante, pero creo que en esta vida se va a quedar sólo en canta-mañanas

               
Loading