Contenido web dinámico en aplicaciones Metro

  • Por
Aplicaciones Metro en HTML5 / JavaScript.
En Windows 8 tenemos un nuevo tipo de aplicaciones: las apps estilo Metro, orientadas especialmente a los dispositivos táctiles y con un aspecto completamente nuevo.

Las apps Metro se pueden desarrollar en muchos lenguajes, entre ellos podemos elegir el conjunto HTML5/CSS3/JavaScript. Estas aplicaciones se ejecutan de forma nativa dentro de un entorno seguro que nos proporciona la plataforma. Esto implica que, tanto el código HTML5/CSS3 como los scripts de JavaScript, se despliegan como un paquete al dispositivo del usuario y se ejecutan en modo local.

Para mejorar la seguridad de nuestra aplicación, se impide la ejecución de código remoto y por lo tanto, no podemos utilizar scripts provenientes de otros sitios si estos no se pueden descargar para ejecutarse de forma offline.

Esto no impide que utilicemos código HTML que provenga de fuera de nuestra aplicación, pues la mayoría de aplicaciones recibirán contenido de fuentes externas, pero para evitar problemas de seguridad, todo el HTML que mostremos en nuestras aplicaciones pasará por la función toStaticHTML.

En el caso de los diversos frameworks JavaScript existentes, como por ejemplo jQuery, esto no supone un problema, pues podemos descargar el código que necesitemos e instalarlo con nuestra aplicación, pero hay algunas aplicaciones en las que es necesario utilizar el código JavaScript que nos proporcionan de forma remota.

Por ejemplo, si queremos utilizar una aplicación de mapas como GoogleMaps u otras parecidas, suelen ejecutar un script sobre un elemento de la página donde se va creando el mapa. El código "Hola Mundo" de los mapas de Google es así:

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<style type="text/css">
html { height: 100% }
body { height: 100%; margin: 0; padding: 0 }
#map_canvas { height: 100% }
</style>
<script type="text/javascript"
src="http://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&sensor=SET_TO_TRUE_OR_FALSE">
</script>
<script type="text/javascript">
function initialize() {
var myOptions = {
center: new google.maps.LatLng(-34.397, 150.644),
zoom: 8,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
var map = new google.maps.Map(document.getElementById("map_canvas"),
myOptions);
}
</script>
</head>
<body onload="initialize()">
<div id="map_canvas" style="width:100%; height:100%"></div>
</body>
</html>

Tenemos un elemento div llamado map_canvas donde aparecerá el mapa tras crearlo utilizando una llamada a la api de google maps. El script que generará el mapa lo debemos descargar de una URL externa. Si intentamos crear una aplicación Metro con este código, recibiremos el siguiente mensaje:

APPHOST9601: Can't load <https://maps.googleapis.com/maps/api/js? sensor=true>. An app can't load remote web content in the local context.
File: default.html

 

La seguridad en las aplicaciones Metro

A bote pronto, esto nos parecerá un gran problema de las aplicaciones Metro, pero tiene mucho sentido que sea así. En Windows 8 el JavaScript de nuestras aplicaciones tiene acceso al hardware de la máquina de una manera mucho más directa que la que tenemos dentro del contexto de un navegador web, pues tiene acceso a toda la librería WinRT que nos permite utilizar dispositivos como la cámara o el GPS, acceder al almacenamiento local de la aplicación y, en definitiva, interactuar con el sistema.
 

Contexto Local vs Contexto Web

Si el sistema permitiera la ejecución directa de JavaScript proveniente de internet, estaría creando un agujero de seguridad muy importante en nuestras aplicaciones. Para evitar esto se han creado dos entornos de ejecución separados que podremos comunicar mediante el envío de mensajes a través del estándar postMessage.

Para crear una aplicación con los dos contextos, tendremos una página (en el gráfico, default.html) que se ejecuta en el contexto local. Dentro de la página colocaremos un elemento iframe que contendrá la página que ejecuta contenido externo (en el gráfico, web.html). Ambas páginas siguen siendo páginas locales, forman parte de nuestro proyecto, e indicaremos a la aplicación que el contenido debe ejecutarse dentro del contexto web con la cabecera ms-appx-web:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>GoogleMaps</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0.RC/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0.RC/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0.RC/js/ui.js"></script>

<!-- GoogleMaps references -->
<link href="/css/default.css" rel="stylesheet" />
<script src="/js/default.js"></script>
</head>
<body>
<iframe id="mapFrame" src="ms-appx-web:///gmap/map.html" style="width:100%; height:100%">
</body>
</html>

Con la cabecera ms-appx-web indicamos que el contexto de ejecución no es el local sino que es un contexto web, en el que la página no tiene acceso a las librerías WinRT. Este contexto sí nos permite la ejecución de scripts remotos, ya que no habrá peligro de interacción con nuestro sistema directamente.

Y ahora sí que podemos introducir en la página un enlace al script de google:

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<style type="text/css">
html { height: 100% }
body { height: 100%; margin: 0; padding: 0 }
#map_canvas { height: 100% }
</style>
<script type="text/javascript"
src="https://maps.googleapis.com/maps/api/js?sensor=true">
</script>
<script type="text/javascript" src="/gmap/map.js"></script>
</head>
<body>
<div id="map_canvas" style="width:100%; height:100%"></div>
</body>
</html>

Nota: es recomendable añadir el parámetro key= YOUR_API_KEY en la llamada al script con nuestra clave del API de Google para mejorar el funcionamiento.

Como es una aplicación Metro, he extraído el código de inicialización al script map.js. Esto ayuda a tener la página y el código más limpios y nos permite que el sistema utilice bytecode caching, incrementando la velocidad de carga:

(function () {
"use strict";
var map;

//initialize maps as in the google api example...
function initialize() {
var myOptions = {
center: new google.maps.LatLng(-34.397, 150.644),
zoom: 8,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map(document.getElementById("map_canvas"),
myOptions);
}

window.addEventListener("DOMContentLoaded", initialize, false);
})();

 

Comunicación entre contextos

Ahora ya podemos ver el mapa dentro de nuestra aplicación Metro y para comunicar con el mapa tendremos que intercambiar mensajes entre las dos páginas, pues ya no tenemos un acceso directo al script. Vamos a conseguir esto mediante la función postMessage y el evento message.

La función postMessage pertenece al objeto window y requiere dos parámetros: el mensaje y el destino. El mensaje será cualquier objeto JavaScript que podamos convertir en literal, es decir un string, un array o un objeto complejo, pero no una función. El contexto tiene que ser siempre de la forma ms-appx-web:///id_de_aplicación cuando queramos enviar un mensaje hacia el iFrame y de la forma ms-appx:/// id_de_aplicación cuando enviemos el mensaje desde el iFrame hacia la ventana que lo contiene.

El evento message lo recibirá el iFrame con un parámetro, dentro de su propiedad data encontraremos el mensaje enviado a través de la función postMessage.

En el siguiente ejemplo vamos a acceder al servicio de localización GPS del equipo y enviaremos la información de localización al iframe para centrar el mapa sobre nuestra posición. Para conseguirlo, vamos primero a obtener la posición del usuario. Necesitaremos activar los servicios de localización en el manifiesto de la aplicación:

Una vez habilitados, añadiremos un botón en la barra de aplicación. Para ello, en default.html añadiremos la barra justo después del elemento iframe:

<div id="appBar" data-win-control="WinJS.UI.AppBar" data-win-options="{sticky:true}">
<button data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{id:'cmdLocation', label:'Mi posición', icon:'world'}">

</button>
</div>

En default.js buscamos la llamada args.setPromise en la función onactivated y la sustituimos por la siguiente:

args.setPromise(WinJS.UI.processAll().then(
function (e) {
//hacemos visible la barra de aplicación
var appBar = document.querySelector("#appBar").winControl;
appBar.show();

//inicializamos el comando de posición
var locationButton = document.querySelector("#cmdLocation");
locationButton.addEventListener("click",
function (e) {
//activamos el sistema de localización
var location = new Windows.Devices.Geolocation.Geolocator();
location.getGeopositionAsync().then(
function (e) {
//al obtener la localización creamos un objeto con la información
var message = {
msg: "coordinates",
lat: e.coordinate.latitude,
long: e.coordinate.longitude,
alt: e.coordinate.altitude
};
//enviamos el mensaje al iframe que contiene el mapa
mapFrame.postMessage(message, "ms-appx-web://" + document.location.host);
});
});

})
);

Utilizando el objeto que hemos creado con el mensaje, podemos enviar información a la página que tenemos dentro del iframe. Para poder utilizar la información, necesitamos una función que la recoja. Creamos para ello un manejador de evento en map.js que recoge el mensaje. Dentro de los parámetros del evento, en la propiedad data, tendremos la información que hemos enviado desde la página principal:

window.addEventListener("message", receiveMessage, false);

var marker;
//recibe mensajes del documento
function receiveMessage(e) {
if (e.origin === "ms-appx://" + document.location.host) {
//dentro de e.data recibimos el objeto enviado por
//default.js desde la llamada postMessage
if (e.data.msg) {
switch (e.data.msg) {
case "coordinates":
var myLatLng = new google.maps.LatLng(e.data.lat, e.data.long);
map.setCenter(myLatLng);
if (!marker) {
marker = new google.maps.Marker({
position: myLatLng,
map: map,
title: "Estas aquí!"
});
}
else {
marker.setPosition(myLatLng);
}
break;
default:
//show error
break;
}
}
}
}

Es conveniente comprobar el origen del mensaje para mejorar la seguridad de nuestras aplicaciones. Podemos encontrar este dato en la propiedad e.origin.

La comunicación también funciona en sentido contrario: desde la página map.html podemos enviar mensajes a la página que la contiene utilizando la misma técnica, así podemos, por ejemplo, enviar las coordenadas sobre las que hemos pulsado a la aplicación para poder realizar operaciones con esa información:

//obtenemos el nombre del host de la aplicación, siempre tenemos que usar este
//para que llegen los mensajes
var locationHost = "ms-appx://" + document.location.host;
//attach to event from the map
google.maps.event.addListener(map, 'click', function (e) {
//creamos un objeto con información para el mensaje
var message = {
msg: "click",
x: e.pixel.x,
y: e.pixel.y,
lat: e.latLng.lat(),
lng: e.latLng.lng()
};
//enviamos el mensaje que hemos creado al contenedor
window.parent.postMessage(message, locationHost);
});

 

Seguridad avanzada

El elemento iframe contiene un atributo llamado sandbox que nos permitirá definir qué permitimos dentro del mismo para evitarnos problemas. Si añadimos dicho atributo quedará deshabilitado todo por defecto: ejecución de scripts, envío de formularios, acceso al DOM del contenedor, etc…

En nuestro caso sólo necesitamos ejecutar scripts, así que podemos deshabilitar el resto de capacidades:

<iframe sandbox="allow-scripts" id="mapFrame" src="ms-appx-web:///gmap/map.html"
style="width:100%; height:100%">

</iframe>

El valor allow-top-navigation no tendrá ningún efecto, pues en aplicaciones metro esto no se permite.
 

Conclusiones

Las aplicaciones Metro cuentan con mecanismos de seguridad parecidos a los que nos impiden en web realizar cross-scripting entre iFrames. La solución también es muy parecida, utilizar el estándar postMessage que nos permite comunicarnos con los scripts del iFrame sin necesidad de ejecutar código desconocido dentro del entorno de nuestra aplicación.

El código de ejemplo de este artículo está disponible en codeplex.

El ejemplo se ha realizado con Google Maps, pues para Bing Maps ya existe un sdk que nos permitirá integrar los mapas en nuestras aplicaciones directamente: msdn.microsoft.com/en-us/library/hh846481.aspx

Nota: para poder ejecutar el código es recomendable utilizar una clave del API, pues obtendremos mejores resultados. La podemos obtener aquí: code.google.com/apis/console

 

¿Qué necesito?

Para desarrollar aplicaciones Metro es suficiente con tener un Windows 8 y las herramientas de desarrollo gratuitas que nos proporciona Microsoft en el sitio dev.windows.com o msdn.microsoft.com/en-US/windows/apps/br229516.aspx

Enlaces (en inglés):

Autor

Juan Manuel Servera

Technical Manager en el Microsoft Innovation Center | Tourism Technologies

Compartir