Lo que el equipo de Bulletin aprendió sobre los service workers mientras desarrollaba una AWP.
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 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 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, examinemos por qué crear una AWP era una opción atractiva para este proyecto:
- Capacidad para 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 forma bastante pareja 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 significaba simplemente 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á.
Lo que 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 Errores de service workers no generados para obtener más información.
No todas las bibliotecas son compatibles con service workers
Algunas bibliotecas de JS hacen suposiciones que no funcionan como se espera cuando las ejecuta un trabajador de servicio. 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. Para esta AWP en particular, queríamos usar gapi.js para la autenticación, pero no pudimos hacerlo porque no admitía service workers. 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:
- El usuario tiene una aplicación web con la versión N de IndexedDB (IDB).
- Se envía la nueva app web con la versión N+1 del IDB.
- El usuario visita la AWP, lo que activa la descarga del nuevo service worker
- 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. - 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.
- 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 dependas 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 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 funcionará 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 lo opuesto a lo que deseas, en particular 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 con las Herramientas para desarrolladores de Chrome si habilitas la casilla de verificación Omitir para la red (panel Aplicación > panel Trabajadores de servicio) además de habilitar la casilla de verificación Inhabilitar caché en el panel Red, a fin de inhabilitar también la memoria caché. 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 la integración continua para alertarte si no cumples con 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 habría evitado eso.
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 abría nuestra app, el service worker entregaba al cliente anterior mientras descargaba el nuevo de forma diferida. Una vez que se descargue el cliente nuevo, se le pedirá al usuario que actualice la página para acceder a 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. 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 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 service worker
A veces, es necesario acceder a los valores de las cookies en un contexto de service worker. 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 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 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 cambie de alguna manera cuando cambie 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 por ti.
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 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 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: