Introducción
Las actualizaciones con imágenes en movimiento, las transiciones de página entrecortadas y las demoras periódicas en los eventos de toque son solo algunos de los problemas de los entornos web móviles actuales. Los desarrolladores intentan acercarse lo más posible a lo nativo, pero a menudo se descarrilan por hacks, restablecimientos y frameworks rígidos.
En este artículo, analizaremos lo mínimo necesario para crear una app web HTML5 para dispositivos móviles. El objetivo principal es desenmascarar las complejidades ocultas que los frameworks para dispositivos móviles actuales intentan ocultar. Verás un enfoque minimalista (con las APIs principales de HTML5) y conceptos básicos que te permitirán escribir tu propio framework o contribuir al que usas actualmente.
Aceleración de hardware
Por lo general, las GPUs controlan los diagramas de CAD o los modelos 3D detallados, pero en este caso, queremos que nuestros dibujos primitivos (divs, fondos, texto con sombras, imágenes, etc.) se vean fluidos y se animen sin problemas a través de la GPU. Lo lamentable es que la mayoría de los desarrolladores front-end presentan este proceso de animación a un marco de terceros sin preocuparse por la semántica, pero ¿deberían enmascararse estas funciones principales de CSS3? Te mostraré algunas razones por las cuales es importante preocuparse por estos temas:
Asignación de memoria y carga computacional: Si compones todos los elementos del DOM solo por la aceleración de hardware, la próxima persona que trabaje en tu código podría perseguirte y derrotarte.
Consumo de energía: Obviamente, cuando entra en juego el hardware, también lo hace la batería. Cuando desarrollan para dispositivos móviles, los desarrolladores se ven obligados a tener en cuenta la amplia variedad de restricciones de los dispositivos mientras escriben apps web para dispositivos móviles. Esto será aún más frecuente a medida que los creadores de navegadores comiencen a habilitar el acceso a más y más hardware de dispositivos.
Conflictos — Encontré un comportamiento glitch al aplicar la aceleración de hardware a partes de la página que ya estaban aceleradas. Por lo tanto, es muy importante saber si tienes una aceleración superpuesta.
Para que la interacción del usuario sea fluida y lo más cercana posible a la nativa, debemos hacer que el navegador funcione para nosotros. Idealmente, queremos que la CPU del dispositivo móvil configure la animación inicial y, luego, que la GPU sea responsable de solo combinar diferentes capas durante el proceso de animación. Esto es lo que hacen translate3d, scale3d y translateZ: les dan a los elementos animados su propia capa, lo que permite que el dispositivo renderice todo de forma fluida. Para obtener más información sobre la composición acelerada y cómo funciona WebKit, Ariya Hidayat tiene mucha información útil en su blog.
Transiciones de página
Veamos tres de los enfoques más comunes de interacción de los usuarios al desarrollar una aplicación web móvil: los efectos de deslizamiento, giro y rotación.
Puedes ver este código en acción aquí: http://slidfast.appspot.com/slide-flip-rotate.html. (Nota: Esta demostración se diseñó para dispositivos móviles, por lo que debes iniciar un emulador, usar tu teléfono o tablet, o reducir el tamaño de la ventana del navegador a ~1024 px o menos).
Primero, desglosaremos las transiciones de deslizamiento, giro y rotación, y cómo se aceleran. Observa cómo cada animación solo toma tres o cuatro líneas de CSS y JavaScript.
Variable
Las transiciones de páginas deslizantes, el más común de los tres enfoques de transición, imitan la sensación nativa de las aplicaciones para dispositivos móviles. Se invoca la transición de diapositivas para llevar un nuevo área de contenido al viewport.
Para el efecto de deslizamiento, primero declaramos nuestro marcado:
<div id="home-page" class="page">
<h1>Home Page</h1>
</div>
<div id="products-page" class="page stage-right">
<h1>Products Page</h1>
</div>
<div id="about-page" class="page stage-left">
<h1>About Page</h1>
</div>
Observa cómo tenemos este concepto de páginas de preparación a la izquierda o a la derecha. En esencia, puede ser cualquier dirección, pero esta es la más común.
Ahora tenemos animación y aceleración de hardware con solo unas pocas líneas de CSS. La animación real ocurre cuando intercambiamos clases en los elementos div de la página.
.page {
position: absolute;
width: 100%;
height: 100%;
/*activate the GPU for compositing each page */
-webkit-transform: translate3d(0, 0, 0);
}
translate3d(0,0,0)
se conoce como el enfoque “bala de plata”.
Cuando el usuario hace clic en un elemento de navegación, ejecutamos el siguiente código JavaScript para intercambiar las clases. No se usan frameworks de terceros, es solo JavaScript. ;)
function getElement(id) {
return document.getElementById(id);
}
function slideTo(id) {
//1.) the page we are bringing into focus dictates how
// the current page will exit. So let's see what classes
// our incoming page is using. We know it will have stage[right|left|etc...]
var classes = getElement(id).className.split(' ');
//2.) decide if the incoming page is assigned to right or left
// (-1 if no match)
var stageType = classes.indexOf('stage-left');
//3.) on initial page load focusPage is null, so we need
// to set the default page which we're currently seeing.
if (FOCUS_PAGE == null) {
// use home page
FOCUS_PAGE = getElement('home-page');
}
//4.) decide how this focused page should exit.
if (stageType > 0) {
FOCUS_PAGE.className = 'page transition stage-right';
} else {
FOCUS_PAGE.className = 'page transition stage-left';
}
//5. refresh/set the global variable
FOCUS_PAGE = getElement(id);
//6. Bring in the new page.
FOCUS_PAGE.className = 'page transition stage-center';
}
stage-left
o stage-right
se convierte en stage-center
y obliga a la página a deslizarse hacia el viewport central. Dependemos completamente de CSS3 para que realice el trabajo pesado.
.stage-left {
left: -480px;
}
.stage-right {
left: 480px;
}
.stage-center {
top: 0;
left: 0;
}
A continuación, veamos el CSS que controla la detección y orientación de dispositivos móviles. Podríamos abordar todos los dispositivos y todas las resoluciones (consulta la resolución de la consulta de medios). En esta demostración, solo usé algunos ejemplos simples para abarcar la mayoría de las vistas verticales y horizontales en dispositivos móviles. Esto también es útil para aplicar la aceleración de hardware por dispositivo. Por ejemplo, dado que la versión para computadoras de escritorio de WebKit acelera todos los elementos transformados (independientemente de si son 2D o 3D), tiene sentido crear una consulta de medios y excluir la aceleración en ese nivel. Ten en cuenta que los trucos de aceleración de hardware no proporcionan ninguna mejora de velocidad en Android Froyo 2.2 y versiones posteriores. Toda la composición se realiza dentro del software.
/* iOS/android phone landscape screen width*/
@media screen and (max-device-width: 480px) and (orientation:landscape) {
.stage-left {
left: -480px;
}
.stage-right {
left: 480px;
}
.page {
width: 480px;
}
}
Giro
En los dispositivos móviles, voltear se conoce como deslizar una página hacia afuera. Aquí, usamos un código JavaScript simple para controlar este evento en dispositivos iOS y Android (basados en WebKit).
Observa cómo funciona http://slidfast.appspot.com/slide-flip-rotate.html.
Cuando trabajes con eventos táctiles y transiciones, lo primero que querrás hacer es obtener el control de la posición actual del elemento. Consulta este documento para obtener más información sobre WebKitCSSMatrix.
function pageMove(event) {
// get position after transform
var curTransform = new WebKitCSSMatrix(window.getComputedStyle(page).webkitTransform);
var pagePosition = curTransform.m41;
}
Dado que usamos una transición de atenuación de CSS3 para el giro de página, el element.offsetLeft
habitual no funcionará.
A continuación, queremos determinar en qué dirección se desliza el usuario y establecer un umbral para que se produzca un evento (navegación de páginas).
if (pagePosition >= 0) {
//moving current page to the right
//so means we're flipping backwards
if ((pagePosition > pageFlipThreshold) || (swipeTime < swipeThreshold)) {
//user wants to go backward
slideDirection = 'right';
} else {
slideDirection = null;
}
} else {
//current page is sliding to the left
if ((swipeTime < swipeThreshold) || (pagePosition < pageFlipThreshold)) {
//user wants to go forward
slideDirection = 'left';
} else {
slideDirection = null;
}
}
También notarás que también medimos el swipeTime
en milisegundos. Esto permite que se active el evento de navegación si el usuario desliza rápidamente la pantalla para pasar a otra página.
Para posicionar la página y hacer que las animaciones se vean nativas mientras un dedo toca la pantalla, usamos transiciones CSS3 después de que se activa cada evento.
function positionPage(end) {
page.style.webkitTransform = 'translate3d('+ currentPos + 'px, 0, 0)';
if (end) {
page.style.WebkitTransition = 'all .4s ease-out';
//page.style.WebkitTransition = 'all .4s cubic-bezier(0,.58,.58,1)'
} else {
page.style.WebkitTransition = 'all .2s ease-out';
}
page.style.WebkitUserSelect = 'none';
}
Intenté jugar con cubic-bezier para dar la mejor sensación nativa a las transiciones, pero la atenuación gradual funcionó.
Por último, para que se realice la navegación, debemos llamar a los métodos slideTo()
definidos anteriormente que usamos en la última demostración.
track.ontouchend = function(event) {
pageMove(event);
if (slideDirection == 'left') {
slideTo('products-page');
} else if (slideDirection == 'right') {
slideTo('home-page');
}
}
Cómo girar el clip
A continuación, veamos la animación de rotación que se usa en esta demostración. En cualquier momento, puedes rotar la página que estás viendo actualmente 180 grados para mostrar el reverso al tocar la opción del menú "Contacto". Una vez más, solo se necesitan unas pocas líneas de CSS y algo de JavaScript para asignar una clase de transición onclick
.
NOTA: La transición de rotación no se renderiza correctamente en la mayoría de las versiones de Android porque carece de capacidades de transformación de CSS en 3D. Lamentablemente, en lugar de ignorar el giro, Android hace que la página se “voltee” girando en lugar de voltearse. Te recomendamos que uses esta transición con moderación hasta que mejore la compatibilidad.
El marcado (concepto básico de frente y dorso):
<div id="front" class="normal">
...
</div>
<div id="back" class="flipped">
<div id="contact-page" class="page">
<h1>Contact Page</h1>
</div>
</div>
El código JavaScript:
function flip(id) {
// get a handle on the flippable region
var front = getElement('front');
var back = getElement('back');
// again, just a simple way to see what the state is
var classes = front.className.split(' ');
var flipped = classes.indexOf('flipped');
if (flipped >= 0) {
// already flipped, so return to original
front.className = 'normal';
back.className = 'flipped';
FLIPPED = false;
} else {
// do the flip
front.className = 'flipped';
back.className = 'normal';
FLIPPED = true;
}
}
El CSS:
/*----------------------------flip transition */
#back,
#front {
position: absolute;
width: 100%;
height: 100%;
-webkit-backface-visibility: hidden;
-webkit-transition-duration: .5s;
-webkit-transform-style: preserve-3d;
}
.normal {
-webkit-transform: rotateY(0deg);
}
.flipped {
-webkit-user-select: element;
-webkit-transform: rotateY(180deg);
}
Cómo depurar la aceleración de hardware
Ahora que ya vimos las transiciones básicas, analicemos la mecánica de cómo funcionan y se combinan.
Para que se lleve a cabo esta sesión de depuración mágica, iniciemos un par de navegadores y el IDE que elijas. Primero, inicia Safari desde la línea de comandos para usar algunas variables de entorno de depuración. Estoy en Mac, por lo que los comandos pueden variar según el SO. Abre la terminal y escribe lo siguiente:
- $> export CA_COLOR_OPAQUE=1
- $> exportar CA_LOG_MEMORY_USAGE=1
- $> /Applications/Safari.app/Contents/MacOS/Safari
Esto inicia Safari con un par de ayudantes de depuración. CA_COLOR_OPAQUE nos muestra qué elementos se combinan o aceleran. CA_LOG_MEMORY_USAGE nos muestra cuánta memoria estamos usando cuando enviamos nuestras operaciones de dibujo al almacenamiento en caché. Esto te indica exactamente cuánto esfuerzo le estás aplicando al dispositivo móvil y, posiblemente, brinda indicios sobre cómo el uso de la GPU podría estar agotando la batería del dispositivo de destino.
Ahora, iniciemos Chrome para ver información de fotogramas por segundo (FPS) de calidad:
- Abre el navegador web Google Chrome.
- En la barra de URL, escribe about:flags.
- Desplázate hacia abajo algunos elementos y haz clic en “Habilitar” para el contador de FPS.
Si ves esta página en tu versión mejorada de Chrome, verás el contador de FPS rojo en la esquina superior izquierda.
Así es como sabemos que la aceleración de hardware se activa. También nos da una idea de cómo se ejecuta la animación y si tienes fugas (animaciones en ejecución continua que deberían detenerse).
Otra forma de visualizar la aceleración de hardware es abrir la misma página en Safari (con las variables de entorno que mencionamos anteriormente). Todos los elementos acelerados del DOM tienen un tono rojo. Esto nos muestra exactamente lo que se está componiendo por capa. Observa que la navegación blanca no es roja porque no está acelerada.
También hay un parámetro de configuración similar para Chrome en about:flags "Bordes de la capa de renderización compuesta".
Otra excelente manera de ver las capas compuestas es ver la demo de hojas que caen de WebKit mientras se aplica este mod.
Por último, para comprender realmente el rendimiento del hardware gráfico de nuestra aplicación, veamos cómo se consume la memoria. Aquí vemos que enviamos 1.38 MB de instrucciones de dibujo a los búferes de CoreAnimation en macOS. Los búferes de memoria de Core Animation se comparten entre OpenGL ES y la GPU para crear los píxeles finales que ves en la pantalla.
Cuando simplemente cambiamos el tamaño de la ventana del navegador o la maximizamos, también vemos que la memoria se expande.
Esto te da una idea de cómo se consume la memoria en tu dispositivo móvil solo si cambias el tamaño del navegador a las dimensiones correctas. Si estabas depurando o probando entornos de iPhone, cambia el tamaño a 480 px por 320 px. Ahora comprendemos exactamente cómo funciona la aceleración de hardware y qué se necesita para depurar. Hay algo que hay que leer sobre este tema, pero ver cómo funcionan los búferes de memoria de la GPU de manera visual realmente pone las cosas en perspectiva.
Detrás de escena: recuperación y almacenamiento en caché
Ahora es el momento de llevar el almacenamiento en caché de nuestra página y recursos al siguiente nivel. Al igual que el enfoque que usan JQuery Mobile y frameworks similares, vamos a recuperar y almacenar en caché nuestras páginas de forma previa con llamadas AJAX simultáneas.
Veamos algunos problemas principales de la Web móvil y los motivos por los que debemos hacerlo:
- Recuperación: La recuperación previa de nuestras páginas permite que los usuarios usen la app sin conexión y que no haya esperas entre las acciones de navegación. Por supuesto, no queremos agotar el ancho de banda del dispositivo cuando se conecta, por lo que debemos usar esta función con moderación.
- Caché: A continuación, queremos un enfoque concurrente o asíncrono cuando se recuperan y almacenan en caché estas páginas. También necesitamos usar localStorage (ya que es compatible con todos los dispositivos), que lamentablemente no es asíncrono.
- AJAX y el análisis de la respuesta: Usar innerHTML() para insertar la respuesta de AJAX en el DOM es peligroso (¿y poco confiable?). En su lugar, utilizamos un mecanismo confiable para la inserción de respuestas AJAX y el manejo de llamadas simultáneas. También aprovechamos algunas funciones nuevas de HTML5 para analizar el
xhr.responseText
.
A partir del código de la demostración de deslizar, girar y rotar, comenzamos agregando algunas páginas secundarias y un vínculo a ellas. Luego, analizaremos los vínculos y crearemos transiciones sobre la marcha.
Mira la demostración de recuperación y almacenamiento en caché aquí.
Como puedes ver, aprovechamos el marcado semántico aquí. Solo un vínculo a otra página. La página secundaria sigue la misma estructura de nodos o clases que la página superior. Podríamos dar un paso más y usar el atributo data-* para los nodos de "página", etc. Y esta es la página de detalles (elemento secundario) ubicada en un archivo HTML separado (/demo2/home-detail.html) que se cargará, almacenará en caché y se configurará para la transición en la carga de la app.
<div id="home-page" class="page">
<h1>Home Page</h1>
<a href="demo2/home-detail.html" class="fetch">Find out more about the home page!</a>
</div>
Ahora, veamos el código JavaScript. Para simplificar, no incluiré ayudantes ni optimizaciones en el código. Todo lo que hacemos aquí es recorrer en bucle un array especificado de nodos del DOM para extraer vínculos para obtenerlos y almacenarlos en caché.
Nota: En esta demostración, se llama a este método fetchAndCache()
cuando se carga la página. Lo volveremos a trabajar en la siguiente sección cuando detectemos la conexión de red y determinemos cuándo se debe llamar.
var fetchAndCache = function() {
// iterate through all nodes in this DOM to find all mobile pages we care about
var pages = document.getElementsByClassName('page');
for (var i = 0; i < pages.length; i++) {
// find all links
var pageLinks = pages[i].getElementsByTagName('a');
for (var j = 0; j < pageLinks.length; j++) {
var link = pageLinks[j];
if (link.hasAttribute('href') &&
//'#' in the href tells us that this page is already loaded in the DOM - and
// that it links to a mobile transition/page
!(/[\#]/g).test(link.href) &&
//check for an explicit class name setting to fetch this link
(link.className.indexOf('fetch') >= 0)) {
//fetch each url concurrently
var ai = new ajax(link,function(text,url){
//insert the new mobile page into the DOM
insertPages(text,url);
});
ai.doGet();
}
}
}
};
Nos aseguramos de que el procesamiento posterior asíncrono sea adecuado mediante el uso del objeto "AJAX". Encontrarás una explicación más avanzada sobre el uso de localStorage dentro de una llamada AJAX en Working Off the Grid with HTML5 offline. En este ejemplo, se muestra el uso básico del almacenamiento en caché en cada solicitud y la entrega de los objetos almacenados en caché cuando el servidor muestra cualquier cosa que no sea una respuesta correcta (200).
function processRequest () {
if (req.readyState == 4) {
if (req.status == 200) {
if (supports_local_storage()) {
localStorage[url] = req.responseText;
}
if (callback) callback(req.responseText,url);
} else {
// There is an error of some kind, use our cached copy (if available).
if (!!localStorage[url]) {
// We have some data cached, return that to the callback.
callback(localStorage[url],url);
return;
}
}
}
}
Lamentablemente, dado que localStorage usa UTF-16 para la codificación de caracteres, cada byte se almacena como 2 bytes, lo que aumenta nuestro límite de almacenamiento de 5 MB a 2.6 MB en total. En la siguiente sección, se revela el motivo por el que se recuperan y almacenan en caché estas páginas o este marcado fuera del alcance de la caché de la aplicación.
Con los avances recientes en el elemento iframe con HTML5, ahora tenemos una forma simple y eficaz de analizar el responseText
que recibimos de nuestra llamada AJAX. Existen muchos analizadores de JavaScript de 3,000 líneas y expresiones regulares que quitan etiquetas de secuencias de comandos, etc. Pero ¿por qué no dejar que el navegador haga lo que mejor sabe hacer? En este ejemplo, escribiremos responseText
en un iframe oculto temporal. Usamos el atributo "zona de pruebas" de HTML5, que inhabilita las secuencias de comandos y ofrece muchas funciones de seguridad…
De la especificación: Cuando se especifica, el atributo de zona de pruebas habilita un conjunto de restricciones adicionales en cualquier contenido alojado por el iframe. Su valor debe ser un conjunto no ordenado de tokens únicos separados por espacios que no distinguen mayúsculas de minúsculas en ASCII. Los valores permitidos son allow-forms, allow-same-origin, allow-scripts y allow-top-navigation. Cuando se establece el atributo, el contenido se considera de un origen único, se inhabilitan los formularios y las secuencias de comandos, se impide que los vínculos se segmenten para otros contextos de navegación y se inhabilitan los complementos.
var insertPages = function(text, originalLink) {
var frame = getFrame();
//write the ajax response text to the frame and let
//the browser do the work
frame.write(text);
//now we have a DOM to work with
var incomingPages = frame.getElementsByClassName('page');
var pageCount = incomingPages.length;
for (var i = 0; i < pageCount; i++) {
//the new page will always be at index 0 because
//the last one just got popped off the stack with appendChild (below)
var newPage = incomingPages[0];
//stage the new pages to the left by default
newPage.className = 'page stage-left';
//find out where to insert
var location = newPage.parentNode.id == 'back' ? 'back' : 'front';
try {
// mobile safari will not allow nodes to be transferred from one DOM to another so
// we must use adoptNode()
document.getElementById(location).appendChild(document.adoptNode(newPage));
} catch(e) {
// todo graceful degradation?
}
}
};
Safari se niega correctamente a mover implícitamente un nodo de un documento a otro. Se genera un error si el nodo secundario nuevo se creó en un documento diferente. Aquí usamos adoptNode
y todo está bien.
Entonces, ¿por qué usar un iframe? ¿Por qué no usar innerHTML? Aunque innerHTML ahora forma parte de la especificación HTML5, es una práctica peligrosa insertar la respuesta de un servidor (bueno o malo) en un área sin verificar. Mientras escribía este artículo, no encontré a nadie que usara algo que no fuera innerHTML. Sé que JQuery lo usa en esencia con un resguardo de anexo solo para la excepción. Y JQuery Mobile también lo usa. Sin embargo, no realicé pruebas intensas relacionadas con el "deja de funcionar de forma aleatoria" de internalHTML, pero sería muy interesante ver todas las plataformas que esto afecta. También sería interesante ver qué enfoque tiene el mejor rendimiento... También he escuchado afirmaciones de ambas partes sobre este tema.
Detección, control y generación de perfiles de tipos de red
Ahora que tenemos la capacidad de almacenar en búfer (o almacenar en caché predictivo) nuestra aplicación web, debemos proporcionar las funciones de detección de conexión adecuadas que la hagan más inteligente. Aquí es donde el desarrollo de apps para dispositivos móviles se vuelve extremadamente sensible a los modos en línea y sin conexión, y a la velocidad de conexión. Ingresa La API de Network Information. Cada vez que muestro esta función en una presentación, alguien del público levanta la mano y pregunta: "¿Para qué usaría eso?". Así que esta es una posible forma de configurar una app web móvil extremadamente inteligente.
Primero, un aburrido escenario de sentido común… Mientras interactúas con la Web desde un dispositivo móvil en un tren de alta velocidad, es muy posible que la red desaparezca en varios momentos y que diferentes geografías admitan diferentes velocidades de transmisión (p. ej., Es posible que HSPA o 3G estén disponibles en algunas áreas urbanas, pero las áreas remotas podrían admitir tecnologías 2G mucho más lentas). El siguiente código aborda la mayoría de los casos de conexión.
El siguiente código proporciona lo siguiente:
- Acceso sin conexión a través de
applicationCache
. - Detecta si se agregó a favoritos y si está sin conexión.
- Detecta cuando se cambia de sin conexión a en línea y viceversa.
- Detecta conexiones lentas y recupera contenido según el tipo de red.
Una vez más, todas estas funciones requieren muy poco código. Primero, detectamos nuestros eventos y situaciones de carga:
window.addEventListener('load', function(e) {
if (navigator.onLine) {
// new page load
processOnline();
} else {
// the app is probably already cached and (maybe) bookmarked...
processOffline();
}
}, false);
window.addEventListener("offline", function(e) {
// we just lost our connection and entered offline mode, disable eternal link
processOffline(e.type);
}, false);
window.addEventListener("online", function(e) {
// just came back online, enable links
processOnline(e.type);
}, false);
En los objetos EventListener anteriores, debemos indicarle a nuestro código si se lo llama desde un evento o una solicitud o actualización de página reales. El motivo principal es que no se activará el evento onload
del cuerpo cuando se cambie entre los modos en línea y sin conexión.
A continuación, tenemos una verificación simple de un evento ononline
o onload
. Este código restablece los vínculos inhabilitados cuando se cambia de la conexión sin conexión a la conexión en línea, pero si esta app fuera más sofisticada, podrías insertar una lógica que reanudara la recuperación de contenido o controlara la UX para conexiones intermitentes.
function processOnline(eventType) {
setupApp();
checkAppCache();
// reset our once disabled offline links
if (eventType) {
for (var i = 0; i < disabledLinks.length; i++) {
disabledLinks[i].onclick = null;
}
}
}
Lo mismo sucede con processOffline()
. Aquí, manipularías tu app para el modo sin conexión y tratarías de recuperar las transacciones que se estaban realizando en segundo plano. El siguiente código busca todos nuestros vínculos externos y los inhabilita, lo que atrapa a los usuarios en nuestra app sin conexión PARA SIEMPRE, muhahaha.
function processOffline() {
setupApp();
// disable external links until we come back - setting the bounds of app
disabledLinks = getUnconvertedLinks(document);
// helper for onlcick below
var onclickHelper = function(e) {
return function(f) {
alert('This app is currently offline and cannot access the hotness');return false;
}
};
for (var i = 0; i < disabledLinks.length; i++) {
if (disabledLinks[i].onclick == null) {
//alert user we're not online
disabledLinks[i].onclick = onclickHelper(disabledLinks[i].href);
}
}
}
Bien, pasemos a lo bueno. Ahora que nuestra app sabe en qué estado de conexión se encuentra, también podemos verificar el tipo de conexión cuando está en línea y ajustarlo según corresponda. Incluí en los comentarios las latencias y las descargas típicas de los proveedores de América del Norte para cada conexión.
function setupApp(){
// create a custom object if navigator.connection isn't available
var connection = navigator.connection || {'type':'0'};
if (connection.type == 2 || connection.type == 1) {
//wifi/ethernet
//Coffee Wifi latency: ~75ms-200ms
//Home Wifi latency: ~25-35ms
//Coffee Wifi DL speed: ~550kbps-650kbps
//Home Wifi DL speed: ~1000kbps-2000kbps
fetchAndCache(true);
} else if (connection.type == 3) {
//edge
//ATT Edge latency: ~400-600ms
//ATT Edge DL speed: ~2-10kbps
fetchAndCache(false);
} else if (connection.type == 2) {
//3g
//ATT 3G latency: ~400ms
//Verizon 3G latency: ~150-250ms
//ATT 3G DL speed: ~60-100kbps
//Verizon 3G DL speed: ~20-70kbps
fetchAndCache(false);
} else {
//unknown
fetchAndCache(true);
}
}
Hay muchos ajustes que podríamos hacer en nuestro proceso fetchAndCache, pero lo único que hice aquí fue decirle que recupere los recursos de forma asíncrona (verdadero) o síncrona (falso) para una conexión determinada.
Cronograma de solicitudes de Edge (síncronas)
Cronograma de solicitudes de WIFI (asíncrono)
Esto permite al menos algún método de ajuste de la experiencia del usuario en función de conexiones lentas o rápidas. De ninguna manera se trata de una solución integral. Otra tarea sería mostrar un diálogo modal de carga cuando se hace clic en un vínculo (en conexiones lentas) mientras la app aún puede recuperar la página de ese vínculo en segundo plano. El objetivo principal es reducir las latencias y aprovechar todas las capacidades de la conexión del usuario con lo último y lo mejor que tiene para ofrecer HTML5. Mira la demostración de detección de red aquí.
Conclusión
El recorrido por el camino de las apps para dispositivos móviles HTML5 acaba de comenzar. Ahora ves los fundamentos muy simples y básicos de un “framework” para dispositivos móviles creado solo en torno a HTML5 y sus tecnologías compatibles. Creo que es importante que los desarrolladores trabajen con estas funciones y las aborden en su esencia, y no las oculten con un wrapper.