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 creaba una AWP orientada a terceros. En estas publicaciones, compartiremos algunos de los desafíos que enfrentamos, los enfoques que adoptamos para superarlos y los 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 del servicio.

Contexto

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, examinemos por qué compilar una AWP fue una opción atractiva para este proyecto:

  • Capacidad de iterar rápidamente. Es especialmente valioso, ya que Bulletin se probaría en varios mercados.
  • Base de código única: Nuestros usuarios se dividieron de manera bastante uniforme 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 y de forma independiente del comportamiento de los usuarios. Las AWP se pueden actualizar automáticamente, lo que reduce la cantidad de clientes desactualizados en el campo. 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 trabajadores de servicio

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.

Si puedes, genéralo

Evita escribir una secuencia de comandos de servicio en primer plano de forma manual. Escribir trabajadores del servicio 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 trabajadores del servicio, 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 trabajador de servicio. Por ejemplo, si se supone que window o document están disponibles, o si se usa una API que no está disponible para los trabajadores de servicio (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 era compatible con los 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 trabajadores del servicio, por ejemplo, evitando las APIs incompatibles con los trabajadores del servicio 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 la nueva app web con la versión N+1 del IDB
  3. El usuario visita la AWP, lo que activa la descarga del nuevo trabajador de servicio
  4. El nuevo service worker 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 trabajador del servicio se bloquea y nunca se instala.

En nuestro caso, la caché se invalidó en la instalación del trabajador de servicio, por lo que, si el trabajador de servicio 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 dependas del estado global

Debido a que los trabajadores de servicio existen en un contexto diferente, no están presentes muchos símbolos que podrías esperar que existieran. 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 ser defensivo con 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 del servicio es almacenar recursos en caché de forma local. Sin embargo, durante el desarrollo, esto es 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 trabajador de servicio, 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é. Es importante incluir el encabezado Cache-Control: no-cache para evitar que el navegador almacene en caché ningún recurso.

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 del 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 trabajadores del servicio se pueden actualizar automáticamente, los usuarios no pueden limitar las actualizaciones. Esto reduce significativamente la cantidad de clientes desactualizados en el campo. Cuando el usuario abrió nuestra app, el trabajador de servicio entregaba 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 de backend sin conexión 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. Como la app se entregaba mientras estaba inactiva, era posible que los clientes más antiguos existieran en el entorno real si el usuario no había abierto la app durante mucho tiempo. En iOS, los trabajadores de servicio 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 ofrecen 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. Siempre 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 este problema, creamos un extremo en nuestro servidor de frontend que simplemente reenviaba el valor de la cookie al cliente. El trabajador del servicio realizó 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 ya no debería ser necesaria para los navegadores que la admiten, ya que proporciona acceso asíncrono a las cookies del navegador y el trabajador del servicio puede usarla directamente.

Problemas con los trabajadores del servicio no generados

Asegúrate de que la secuencia de comandos del servicio trabajador 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 trabajadores del servicio solo se instalan cuando el navegador detecta que la secuencia de comandos del trabajador del servicio cambió de alguna manera, por lo que tuvimos que asegurarnos de que el archivo de secuencia de comandos del trabajador del servicio 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 trabajador de servicio, de modo que cada versión generara un archivo JavaScript de trabajador de servicio distinto. Las bibliotecas de trabajadores del servicio, como Workbox, automatizan este proceso por ti.

Pruebas de unidades

Las APIs de los trabajadores del servicio funcionan agregando objetos de escucha de eventos al objeto global. Por ejemplo:

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

Esto puede ser un dolor de cabeza para probar, ya que debes simular el activador del evento, el objeto del evento, esperar la devolución de llamada de respondWith() y, luego, esperar 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 trabajador de servicio, mantuvimos la secuencia de comandos principal del trabajador de servicio 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: