Capítulo 3 - Los patrones App Shell y PRPL

Hola de nuevo. Terminamos con este post la introducción a PWA. Por ahora, lo único que PWA nos está aportando es un lenguaje nuevo y una manera de clasificar todo aquello que un buen equipo front debería tener en cuenta en sus desarrollos.

Una cosa que me gusta de PWA y de lo que está haciendo Google es que está creando un repositorio de funcionalidades transversales que muchas veces, desde negocio, no se dan como importantes por no centrarse en el dominio, pero que pueden afectar mucho a que un producto sea un éxito y que atraiga a más usuarios.

En el post de hoy nos centraremos en solucionar otro punto del checklist. En esta ocasión solucionaremos este:

La primera carga de nuestra aplicación es rápida hasta con 3G

Para solucionarlo, necesitamos explicar qué son el modelo App Shell y el patrón PRPL. Dos términos nuevos que explican algo que, quizá, no nos sea es tan nuevo :)

Veamos:

¿Qué es el modelo App Shell?

PWA está muy preocupado en que la experiencia de nuestra Web, en cualquier dispositivo, sea la idónea. Si observamos en cómo ha aumentado el consumo de Internet desde el dispositivo móviles en los últimos años, acertaremos si pensamos que es algo que le preocupa mucho a Google.

Mejorar la experiencia de usuario de la Web en dispositivos móviles es algo prioritario ahora mismo. Para ello, es importante que la experiencia sea lo más parecida a la de un aplicativo nativo. Por tanto, pensemos en los elementos que hacen que una aplicación nativa conformen una experiencia óptima de uso.

Se me ocurre lo siguiente:

  • Las aplicaciones nativas son rápidas. Tu accedes desde el menú principal de tu móvil a una aplicación y en cuestión de un segundo tienes tu interfaz cargada lista para ser usada.

  • Optimiza los recursos del usuario . Cuando un usuario se descarga una aplicación, solo se descarga los binarios en su dispositivo una vez. Esto es algo que, por la propia naturaleza Web, era complicado de llevar a cabo hace años. Cuando un usuario necesita usar una Web periódicamente, la descarga de recursos es recurrente y por tanto más costosa - cada vez que consulto la Web, su HTML, CSS, JS y assets son descargados.

  • Tiene una inferfaz común para todas las pantallas. Vale, el núcleo central de cada pantalla cambia, pero los menús, las barras de navegación y los pies de pantalla se mantienen. En Web no tiene porque ser así, o antiguamente no se tenía tan en cuenta esto.

Leyendo estas características, seguro que muchos de vosotros y vosotras tenéis una y mil soluciones para acabar con estos problemas en la Web. Los problemas que planteo suenan del mundo viejuno, sin embargo, siendo sinceros, no todas las Web han evolucionado como debieran y aunque esto suena obvio, hay mucho trabajo que hacer.

Por ejemplo, las arquitecturas front de aplicaciones de única página, o SPAs como suelen conocerse, vinieron para solucionar muchos de estos problemas:

  • Esta arquitectura nos permite - por fin - crear esta estructura principal que homogeneice la interfaz . Ya no tengo que crear sistemas de renderizado complicados para tener un único menú, barra de navegación o pie de página. Las librerías de enrutado ya nos proporcionan esta funcionalidad.

  • Los sistemas de cacheo de HTML5 ya nos permitían cachear nuestras aplicaciones en el navegador.

  • Y la velocidad dependía mucho del navegador y el código que optimizásemos. Minificamos y ofuscamos y optimizamos las imágenes para ganar micro segundos y estamos más concienciados en trabajar sobre esta partes de lo que lo hacíamos antes.

Pero las SPAs nos han traído problemas que estamos intentando solucionar. Por ejemplo:

  • Crear estos sistemas y con las herramientas que contábamos, nos ha obligado a empaquetar toda nuestra aplicación (sí, toda) en un único fichero JavaScript. Con lo que esto supone claro. Ya no solo estoy obligando a que el usuario se descargue todo el rato mi Web, sino que encima le obligo a descargarse todo, aunque no vaya a hacer uso de toda la funcionalidad.

  • Los manifests de HTML5 para indicar el cacheo de recursos nos a ayudado, pero la experiencia de desarrollo en muchos casos nos ha dado problemas. Se nos ha complicado bastante el actualizar estos ficheros cacheados cuando hemos querido añadir nueva funcionalidad o solucionar problemas.

Por tanto, Google ha pensado en un término que está a medio camino entre el no hacer nada por ayudar al usuario y el matar moscas como cañonazos como hacen las SPAs.

Además, estas nuevas técnicas se apoyan en las nuevas APIs para sacar mayor partido al desarrollo y aunar por fin experiencia tanto de usuario como de desarrollo.

Es lo que se conoce como patrón App Shell o modelo App Shell. Compactar la mínima funcionalidad común para todas la pantallas de nuestra aplicación, ya sea HTML, CSS y JS, y cachearla por medio de Services Workers y Cache API.

El resto de la aplicación, se irá cargando bajo demanda del usuario. A nivel de interfaz se trata de extrapolarlo de esta manera:

Todo lo que es App Shell se compilará junto y se cacheará y todo lo que sea contenido se irá cargando bajo demanda ya sea de manera manual o por medio de herramientas automatizadas de cacheo.

¿Qué es el patrón PRPL?

Por otro lado, tenemos lo que se está considerado por Google como patrón PRPL. Este patrón esta en fase de estudio y se trata más de una idea que abarca conceptos sobre optimización de recursos que de algo mucho más elaborado y abstracto.

Se trata de cómo realizar diferentes acciones para que el usuario final disfrute del contenido en el menor tiempo posible y en cualquier dispositivo del mundo real.

PRPL son las siglas de:

  • Push o empaquetado de aquellos recursos que sean indispensables (esto es App Shell) para el inicio de una aplicación,
  • Render o el renderizado de las primeras vistas lo antes posible,
  • Pre-cache o cacheado de aquellos recursos que nosotros intuyamos que van a ser usados en breve por el usuario,
  • Lazy-load o carga de recursos bajo demanda y según el usuario los vaya necesitando.

Si nos damos cuenta, sigue mucho de los pasos de lo que ya aporta App Shell y de lo que las nuevas SPAs intentan hacer. Digamos que PRPL abarca técnicas que ayudan a que seamos proactivos a la hora de cachear y cargar de manera dinámica y perezosa recursos de la aplicación.

Es decir, si sabemos que desde una pantalla en particular se puede navegar a otras dos pantallas que todavía no se han cargado, quizá sería una buena idea que nosotros como desarrolladores nos encargásemos de pedir esos recursos a nuestros servidores, que los rendericemos o que ya vengan renderizados - mejor opción - y cachearlos en memoria, liberando, si hiciese falta claro, recursos ya utilizados.

Todo esto desde procesos que se ejecuten en segundo plano. Si nos damos cuenta, seguimos sacando partido de Services Workers y API Caché e intentamos aprovechar cada una de sus posibilidades para conseguir que lo que el usuario demanda, se cargué en el menor tiempo posible.

Sobre este patrón seguirá saliendo mucha documentación, muchos recursos y librerías que nos guiarán para conseguir estos casos de uso.

Un poco de código

Ahora bien ¿Cómo se materializa todo esto de App Shell y PRPL a la hora de desarrollar? Veamos por ejemplo cómo VueJS lo lleva a cabo.

AVISO: Crear un App Shell (o paquete mínimo cacheable) y aplicar PRPL no tiene nada que ver con el framework JavaScript que uses. En el ejemplo uso Vue por mi propia comodidad, pero cualquiera de las cosas que hago con VueJS las puedes hacer a mano o con tu framework favorito. VueJS no es una librería específica para implementar PWA, pero si está muy concienciada con este tipo de técnicas.

Para el ejemplo, vamos a usar la plantilla de vue-cli que la comunidad tiene para PWA. Para ello ejecutamos el siguiente comando (recuerda tener instalado vue-cli):

$ vue init pwa app-shell

Esto nos crea un proyecto con lo mínimo PWA necesario con nombre app-shell.

Lo primero que podemos observar es cómo la plantilla se encarga de crear un armazón mínimo donde se cargue el Header, Footer y demás componentes comunes. Vayamos al componentes App.vue. Este componente es el encargado de renderizar la layout por defecto de la app.

https://gist.github.com/jdonsan/63f447d7ed8892b930ae8746e319bdd1

Como ya vimos en el manual de Vue, lo importante de este layout HTML es la etiqueta router-view. Esta etiqueta de Vue es un componente dinámico. Es la zona en blanco que estamos reservado para cargar el contenido de la app. El resto de ese template, es lo que podríamos considerar el App Shell.

Vayamos ahora al router:

// router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Hello from '@/components/Hello'

Vue.use(Router)

export default new Router({
    routes: [
        {
            path:'/',
            name:'Hello',
            component:Hello
        }
    ]
})

Este fichero se encarga de configurar todas las rutas disponibles en nuestra Web. En el futuro nos resolverá unos de los requisitos del checklist, pero bueno... por ahora olvidemos eso y centrémonos.

Es una parte importante porque configurar las rutas y decidir qué componente se tiene que renderizar es la clave para poder partir nuestro monolito SPA en partes modularizables. Por ejemplo, añadamos una vista más:

import Vue from 'vue'
import Router from 'vue-router'
import Hello from '@/components/Hello'
import Detail from '@/components/Detail'

Vue.use(Router)

export default new Router({
    routes: [
        {
            path: '/',
            name: 'Hello',
            component: Hello
        },
        {
            path: '/detail',
            name: 'Detail',
            component: Detail
         }
     ]
})

Lo que hacemos ahora es añadir una nueva pantalla. Cuando el usuario indique en la URL /detail, se renderizará este componente Detail que simula una nueva ventana de detalle y cuando ponga /, volveremos a la pantalla principal.

Esto es un primer paso pero no es el definitivo. Si dejamos esto así y construimos nuestro paquete de producción (npm run build) conseguiremos algo como esto:

Está bien porque Webpack (con la configuración por defecto) ha sabido separarme en diferentes ficheros lo que es lógica de aplicacion (app), de lo que son librerías de terceros (vendor) y el motor de dependencias de Webpack (manifest), pero no estamos cumpliendo las directrices de App Shell. App sigue teniendo código innecesario. El código de Detail y Home, no nos es necesario de primeras.

Para solucionar esto, hacemos uso de 'ES Modules' de ES6 y 'Lazy Load' de Webpack. Esto es tan sencillo como poner en router lo siguiente:

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
    routes: [
        {
            path: '/',
            name: 'Hello',
            component: () => import('@/components/Hello')
        },
        {
            path: '/detail',
            name: 'Detail',
            component: () => import('@/components/Detail')
        }
    ]
})

Ahora en el parámetro component, indicamos una factoría con una arrow function y usamos el import de esta manera, la carga es en diferido y solo se hará cuando el usuario vaya a esa vista. Esto Webpack sabe interpretarlo como una rotura del módulo y nos genera lo siguiente:

Seguimos teniendo los ficheros app, vendor y manifest, pero tenemos cosas nuevas: 0 y 1 son los ficheros de cada una de las pantallas que hemos hecho que se carguen de manera perezosa. Esto ya es contenido y no App Shell, hemos conseguido esa separación que las SPAs de otra generación no conseguían por no contar con 'ES Modules', ni Webpack (o similares).

Lo único que nos queda es cachearlo en el navegador del usuario. Esto también es muy cómodo para el desarrollador y debería llevarnos poco trabajo. Tenemos dos maneras de crear este cacheo: el manual y el automático. Yo recomiendo el automático, pero expliquemos los dos:

Cacheo de recursos de manera manual

Lo que hacemos en este caso es unas la API de caché dentro del evento install del service worker:

const cacheName = 'shell-content';
const filesToCache = [
    '/static/js/app.2cbc45bd2965419a4043.js',
    '/static/js/vendor.fa1ea1033599b0840fdf.js',
    '/static/js/manifest.522a073e0941aec05df8.js',
    '/static/css/app.b8e908dd1983cadb5682ced0bf5a9f82.css'
];

self.addEventListener('install', function(e) {
    e.waitUntil(
        caches.open(cacheName).then(function(cache) {
            return cache.addAll(filesToCache);
        })
    );
});

Es un sistema muy poco cómodo porque indicamos en cada momento que cacheamos. Tenemos un control de lo que se cachea y de lo que no, pero nos dificulta el cambio de nombrado de hashes.

¿Y si cuando construimos nuestra aplicación, pudiésemos pre-cachear ya todo lo que se indique de una manera más dinámica y no tan forzada?

Cacheo de recursos de manera automática

La gente de Google ha creado una librería que se llama sw-precache es una librería que te habilita un Service Worker para cachear los recursos.

La librería se puede usar tanto en Gulp, como Grunt, pero yo os voy a mostrar cómo se usa en Webpack. Vayamos al fichero build/webpack.prod.conf.js que es el fichero que se encarga de construir nuestra aplicación para cuando queremos desplegar en producción.

Vayamos a la parte de plugins y observemos estas líneas:

// build/webpack.prod.conf.js

...
newSWPrecacheWebpackPlugin({
    cacheId:'my-vue-app',
    filename:'service-worker.js',
    staticFileGlobs: ['dist/**/*.{js,html,css}'],
    minify:true,
    stripPrefix:'dist/'
})
...

Esta es la abstracción de la librería sw-precache por parte de Webpack. Lo que se nos pide es: los ficheros a cachear (staticFileGlobs), el nombre con el fichero que se va a generar (filename) y dónde va a colocarlo (stripPrefix). Esto, una vez que se compile, creará un fichero /dist/service-worker.js.

Lo único que nos queda es cargarlo en el index.html. Esto lo hacemos así:

// build/webpack.prod.conf.js

...
new HtmlWebpackPlugin({
    ...
    serviceWorkerLoader:`
        <script>${loadMinified(path.join(__dirname,'./service-worker-prod.js'))}<script>
    `
}),
...

Que lo único que hace es incrustar el fichero dentro del index.html en donde se encuentre la etiqueta serviceWorkerLoader.

Y ya está. Ejecuta el ejemplo (usa http-server, por ejemplo), pulsa F5 varias veces y comprobarás en 'Developer Tools' en la pestaña de 'Network' que los recursos son cargados desde la caché del navegador.

Conclusión

Parece que la propia plantilla de Vue para cumplir con el checklist de Google ya tiene en cuenta todo esto :). Las nuevas plataformas de desarrollo front como ReactJS, Angular o VueJS ya tienen muy en cuenta todas estás técnicas.

Webpack y otros empaquetadores de aplicaciones llevan unos años ayudando a que esto sea posible y los Services Workers y la API Caché son la puntilla que nos faltaba para por fin ser competentes contra aplicaciones nativas.

Los conceptos no son todo lo innovador que parecían, pero están ahí y nos permiten crear un lenguaje fácil que otros perfiles menos técnicos quizá entiendan mejor. Es importante concienciar a todos los integrantes de un equipo que este tipo de técnicas son importantes, por tanto, el estandarizar y facilitar el conocimiento es algo que creo prioritario.

Aquí terminamos la introducción. En los dos siguientes módulos nos pondremos más técnicos y por fin nos centraremos en aprender todo lo necesario sobre Service Workers y API Caché.

Nos leemos :)

results matching ""

    No results matching ""