Cómo compilar una AWP en Google (parte 1)

Lo que el equipo de Bulletin aprendió sobre los service workers mientras desarrollaba una AWP.

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

Esta es la primera de una serie de entradas de blog sobre las lecciones que aprendió el equipo de Google Bulletin mientras compilaba una AWP orientada a terceros. En estas publicaciones, compartiremos algunos de los desafíos que enfrentamos, los enfoques que adoptamos para superarlos y consejos generales para evitar dificultades. Esta no es una descripción general completa de las AWP. El objetivo es compartir los aprendizajes de la experiencia de nuestro equipo.

En esta primera publicación, primero repasaremos un poco de información general y, luego, analizaremos todo lo que aprendimos sobre los trabajadores de servicio.

Segundo plano

El boletín estuvo en desarrollo activo desde mediados de 2017 hasta mediados de 2019.

Por qué elegimos crear una AWP

Antes de profundizar en el proceso de desarrollo, analicemos por qué compilar una AWP fue una opción atractiva para este proyecto:

  • Capacidad de iterar rápidamente. Esto es particularmente valioso, ya que se realizaría una prueba piloto en varios mercados.
  • Base de código única. Nuestros usuarios estaban divididos de manera bastante equitativa entre Android y iOS. Una AWP significaba que podíamos compilar una sola app web que funcionara en ambas plataformas. Esto aumentó la velocidad y el impacto del equipo.
  • Se actualizan rápidamente, sin importar el comportamiento del usuario. Las AWP pueden actualizarse automáticamente, lo que reduce la cantidad de clientes desactualizados en el exterior. Pudimos implementar cambios de backend catastróficos con un tiempo de migración muy corto para los clientes.
  • Se integra fácilmente con apps propias y de terceros. Esas integraciones eran un requisito para la app. Con una AWP, a menudo, solo se trataba de abrir una URL.
  • Se quitó la dificultad de instalar una app.

Nuestro marco de trabajo

Para Boletín, usamos Polymer, pero cualquier framework moderno y bien admitido funcionará.

Qué aprendimos sobre los service workers

No puedes tener una AWP sin un trabajador de servicio. Los trabajadores de servicio te brindan mucha potencia, como estrategias avanzadas de almacenamiento en caché, capacidades sin conexión, sincronización en segundo plano, etc. Si bien los trabajadores de servicio agregan cierta complejidad, descubrimos que sus beneficios superaron la complejidad agregada.

Generarla si puedes

Evita escribir una secuencia de comandos de servicio en primer plano de forma manual. Escribir service workers de forma manual requiere administrar manualmente los recursos almacenados en caché y reescribir la lógica que es común a la mayoría de las bibliotecas de service workers, como Workbox.

Dicho esto, debido a nuestra pila de tecnología interna, no pudimos usar una biblioteca para generar y administrar nuestro trabajador de servicio. A veces, nuestros aprendizajes a continuación lo reflejarán. Consulta Problemas de los trabajadores del servicio no generados para obtener más información.

No todas las bibliotecas son compatibles con los trabajadores del servicio.

Algunas bibliotecas de JS hacen suposiciones que no funcionan como se espera cuando las ejecuta un service worker. Por ejemplo, si suponemos que window o document están disponibles, o si usas una API no disponible para los service workers (XMLHttpRequest, almacenamiento local, etcétera). Asegúrate de que las bibliotecas críticas que necesites para tu aplicación sean compatibles con el servicio de trabajador. En esta PWA en particular, queríamos usar gapi.js para la autenticación, pero no pudimos porque no admitía trabajadores del servicio. Los autores de bibliotecas también deben reducir o quitar las suposiciones innecesarias sobre el contexto de JavaScript siempre que sea posible para admitir casos de uso de service worker, por ejemplo, evitando las APIs incompatibles con service worker y evitando el estado global.

Evita acceder a IndexedDB durante la inicialización

No leas IndexedDB cuando inicialices la secuencia de comandos del trabajador de servicio. De lo contrario, podrías encontrarte con esta situación no deseada:

  1. El usuario tiene una app web con la versión N de IndexedDB (IDB)
  2. Se envía una nueva aplicación web con la versión N+1 de IDB
  3. El usuario visita la AWP, lo que activa la descarga del nuevo service worker
  4. El nuevo servicio trabajador lee de IDB antes de registrar el controlador de eventos install, lo que activa un ciclo de actualización de IDB para pasar de N a N+1.
  5. Como el usuario tiene un cliente anterior con la versión N, el proceso de actualización del trabajador de servicio se bloquea, ya que las conexiones activas aún están abiertas a la versión anterior de la base de datos.
  6. El service worker se bloquea y nunca se instala

En nuestro caso, la caché se invalidó en la instalación del servicio de trabajo, por lo que, si el servicio de trabajo nunca se instaló, los usuarios nunca recibieron la app actualizada.

Haz que sea resiliente

Aunque las secuencias de comandos de los trabajadores del servicio se ejecutan en segundo plano, también se pueden finalizar en cualquier momento, incluso cuando están en medio de operaciones de E/S (red, IDB, etcétera). Cualquier proceso de larga duración se debe poder reanudar en cualquier momento.

En el caso de un proceso de sincronización que subió archivos grandes al servidor y los guardó en el IDB, nuestra solución para las cargas parciales interrumpidas fue aprovechar el sistema reanudable de nuestra biblioteca de carga interna, guardar la URL de carga reanudable en el IDB antes de la carga y usar esa URL para reanudar una carga si no se completaba la primera vez. Además, antes de cualquier operación de E/S de larga duración, el estado se guardaba en el IDB para indicar en qué parte del proceso estábamos para cada registro.

No depender del estado global

Debido a que los trabajadores de servicio existen en un contexto diferente, muchos símbolos que podrías esperar que existan no están presentes. Gran parte de nuestro código se ejecutó en un contexto de window y en un contexto de trabajador de servicio (como el registro, las marcas, la sincronización, etcétera). El código debe estar a la defensiva sobre los servicios que usa, como el almacenamiento local o las cookies. Puedes usar globalThis para hacer referencia al objeto global de una manera que funcione en todos los contextos. Además, usa los datos almacenados en variables globales con moderación, ya que no hay garantía de cuándo se finalizará la secuencia de comandos y se desalojará el estado.

Desarrollo local

Un componente importante de los trabajadores de servicio es almacenar recursos en caché de forma local. Sin embargo, durante el desarrollo, esto es exactamente lo opuesto a lo que deseas, en especial cuando las actualizaciones se realizan de forma diferida. Aún quieres que el trabajador del servidor esté instalado para que puedas depurar problemas con él o trabajar con otras APIs, como la sincronización en segundo plano o las notificaciones. En Chrome, puedes lograr esto a través de las Herramientas para desarrolladores de Chrome. Para ello, habilita la casilla de verificación Omitir para la red (panel Application > panel Service workers) y, además, habilita la casilla de verificación Disable cache en el panel Network para inhabilitar también la caché de memoria. Para abarcar más navegadores, optamos por una solución diferente, que incluye una marca para inhabilitar el almacenamiento en caché en nuestro servicio trabajador, que está habilitado de forma predeterminada en las compilaciones para desarrolladores. Esto garantiza que los desarrolladores siempre obtengan sus cambios más recientes sin problemas de almacenamiento en caché. También es importante incluir el encabezado Cache-Control: no-cache para evitar que el navegador almacene en caché los elementos.

Faro

Lighthouse proporciona varias herramientas de depuración útiles para las AWP. Analiza un sitio y genera informes sobre las AWP, el rendimiento, la accesibilidad, el SEO y otras prácticas recomendadas. Te recomendamos que ejecutes Lighthouse en una integración continua para que te alerten si incumples uno de los criterios para ser una AWP. Esto nos sucedió una vez, cuando el trabajador de servicio no se instalaba y no nos dimos cuenta antes de un envío a producción. Tener Lighthouse como parte de nuestra CI lo habría prevento.

Adopta la entrega continua

Debido a que los service workers pueden actualizarse automáticamente, los usuarios no tienen la capacidad de limitar las actualizaciones. Esto reduce significativamente la cantidad de clientes desactualizados en el campo. Cuando el usuario abrió nuestra app, el service worker entregó el cliente anterior mientras descargaba de forma diferida el cliente nuevo. Una vez que se descargara el cliente nuevo, se le solicitaría al usuario que actualice la página para acceder a las funciones nuevas. Incluso si el usuario ignorara esta solicitud, la próxima vez que actualice la página recibirá la versión nueva del cliente. Como resultado, es bastante difícil para un usuario rechazar las actualizaciones de la misma manera que lo hace con las apps para iOS o Android.

Pudimos enviar cambios rotundos en el backend con un tiempo de migración muy corto para los clientes. Por lo general, les damos un mes a los usuarios para que se actualicen a clientes más nuevos antes de realizar cambios drásticos. Dado que la app se publicaría mientras estaba inactiva, era posible que los clientes más antiguos existieran si el usuario no la abriera durante mucho tiempo. En iOS, los service workers se expulsan después de un par de semanas, por lo que no ocurre este caso. En el caso de Android, este problema se puede mitigar si no se publica mientras está inactivo o si se vence el contenido de forma manual después de algunas semanas. En la práctica, nunca tuvimos problemas con clientes inactivos. El nivel de rigurosidad que un equipo determinado quiera aplicar depende de su caso de uso específico, pero las AWP proporcionan mucha más flexibilidad que las apps para iOS o Android.

Cómo obtener valores de cookies en un trabajador de servicio

A veces, es necesario acceder a los valores de las cookies en un contexto de trabajador de servicio. En nuestro caso, necesitábamos acceder a los valores de las cookies para generar un token que autenticara las solicitudes a la API propias. En un trabajador de servicio, las APIs síncronas, como document.cookies, no están disponibles. Puedes enviar un mensaje a los clientes activos (con ventanas) desde el trabajador de servicio para solicitar los valores de las cookies, aunque es posible que el trabajador de servicio se ejecute en segundo plano sin ningún cliente con ventanas disponible, como durante una sincronización en segundo plano. Para solucionar esto, creamos un extremo en nuestro servidor de frontend que simplemente replicaba el valor de la cookie al cliente. El service worker envió una solicitud de red a este extremo y leyó la respuesta para obtener los valores de la cookie.

Con el lanzamiento de la API de Cookie Store, esta solución alternativa ya no debería ser necesaria para los navegadores compatibles, ya que proporciona acceso asíncrono a las cookies del navegador y el service worker puede usarla directamente.

Problemas con los trabajadores del servicio no generados

Asegúrate de que la secuencia de comandos del servicio de trabajo cambie si cambia algún archivo almacenado en caché estático

Un patrón común de AWP es que un trabajador de servicio instale todos los archivos estáticos de la aplicación durante su fase install, lo que permite que los clientes accedan directamente a la caché de la API de Cache Storage para todas las visitas posteriores. Los service workers solo se instalan cuando el navegador detecta que la secuencia de comandos del service worker cambió de alguna manera, por lo que tuvimos que asegurarnos de que el archivo de secuencia de comandos del service worker cambiara de alguna manera cuando cambió un archivo almacenado en caché. Para ello, incorporamos un hash del conjunto de archivos de recursos estáticos en nuestra secuencia de comandos de service worker, de modo que cada versión generara un archivo de JavaScript de service worker distinto. Las bibliotecas de service worker, como Workbox, automatizan este proceso.

Pruebas de unidades

Las APIs de los servicios de trabajo agregan objetos de escucha de eventos al objeto global. Por ejemplo:

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

Esto puede ser difícil de probar, ya que debes simular el activador del evento, el objeto de evento, esperar la devolución de llamada respondWith() y, luego, esperar a la promesa antes de confirmar el resultado. Una manera más fácil de estructurar esto es delegar toda la implementación a otro archivo, que se puede probar con mayor facilidad.

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

Debido a las dificultades de realizar pruebas de unidades en una secuencia de comandos de service worker, mantuvimos la secuencia de comandos principal del service worker lo más simple posible y dividimos la mayor parte de la implementación en otros módulos. Dado que esos archivos eran solo módulos JS estándar, se podían probar en unidades con mayor facilidad con bibliotecas de pruebas estándar.

No te pierdas las partes 2 y 3

En las partes 2 y 3 de esta serie, hablaremos sobre la administración de contenido multimedia y los problemas específicos de iOS. Si quieres obtener más información sobre cómo compilar una AWP en Google, visita nuestros perfiles de autor para descubrir cómo comunicarte con nosotros: