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

Qué aprendió el equipo de boletines 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 Boletín durante la compilación de una AWP externa. En estas publicaciones, compartiremos algunos de los desafíos que enfrentamos, los enfoques que tomamos para superarlos y algunos consejos generales para evitar errores. No se trata de una descripción general completa de las AWP. El objetivo es compartir lo aprendido en la experiencia de nuestro equipo.

En esta primera publicación, primero abordaremos un poco de información general y, luego, profundizaremos en todo lo que aprendimos sobre los service workers.

Información general

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. Esto es especialmente valioso, ya que Boletín se lanzaría en varios mercados.
  • Base de código única. La cantidad de usuarios se dividió de manera equitativa entre iOS y Android. Con una AWP, podíamos compilar una sola app web que funcionara en ambas plataformas. Esto aumentó la velocidad e impacto del equipo.
  • Se actualizan rápidamente y son independientes del comportamiento del usuario. Las AWP pueden actualizarse automáticamente, lo que reduce la cantidad de clientes desactualizados en el entorno. Pudimos implementar cambios rotundos de backend con muy poco tiempo de migración para los clientes.
  • Se integra fácilmente en apps propias y de terceros. Esas integraciones eran un requisito para la app. Con una AWP, a menudo significaba simplemente abrir una URL.
  • Eliminé las complicaciones de la instalación de una app.

Nuestro marco

Para Boletín, usamos Polymer, pero cualquier marco de trabajo moderno y con buena compatibilidad funcionará.

Qué aprendimos sobre los service workers

No puedes tener una AWP sin un service worker. Los service workers te proporcionan mucha potencia, como estrategias avanzadas de almacenamiento en caché, capacidades sin conexión, sincronización en segundo plano, etc. Si bien los service workers agregan algo de complejidad, descubrimos que sus beneficios superan la complejidad adicional.

Generalo si puedes

Evita escribir una secuencia de comandos de service worker de forma manual. La escritura manual de los service workers requiere la administración manual de los recursos almacenados en caché y la reescritura de la lógica común para la mayoría de las bibliotecas de service worker, como Workbox.

Dicho esto, debido a nuestra pila tecnológica interna, no podríamos usar una biblioteca para generar y administrar nuestro service worker. En ocasiones, lo que aprendiste lo reflejan los siguientes aprendizajes. Para obtener más información, ve a Errores de service worker no generados.

No todas las bibliotecas son compatibles con service-worker.

Algunas bibliotecas JS hacen suposiciones que no funcionan según lo esperado cuando las ejecuta un service worker. Por ejemplo, suponiendo que window o document estén disponibles, o bien si usas una API no disponible para los service workers (XMLHttpRequest, almacenamiento local, etc.). Asegúrate de que las bibliotecas esenciales que necesitas para tu aplicación sean compatibles con service-worker. 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 cuando sea posible para admitir casos de uso de service worker, como evitar las APIs incompatibles con service worker y evitar el estado global.

Evita acceder a IndexedDB durante la inicialización

No leas IndexedDB cuando inicialices la secuencia de comandos de tu service worker, o podrías caer en esta situación no deseada:

  1. El usuario tiene una app web con IndexedDB (IDB) versión N
  2. Se envía la 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 service worker nuevo lee del IDB antes de registrar el controlador de eventos install, lo que activa un ciclo de actualización de IDB para que pase de N a N + 1
  5. Dado que el usuario tiene un cliente anterior con la versión N, se detiene el proceso de actualización del service worker, ya que las conexiones activas siguen abiertas a la versión anterior de la base de datos.
  6. El service worker se bloquea y nunca se instala

En nuestro caso, se invalidó la caché durante la instalación del service worker, por lo que, si este nunca se instaló, los usuarios nunca recibieron la app actualizada.

Haz que sea resiliente

Aunque las secuencias de comandos de service worker se ejecutan en segundo plano, también se pueden finalizar en cualquier momento, incluso en medio de operaciones de E/S (red, IDB, etc.). Cualquier proceso de larga duración debe poder reanudarse en cualquier momento.

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

No dependas del estado global

Debido a que los service workers existen en un contexto diferente, muchos de los 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 service worker (como registros, marcas, sincronización, etc.). El código debe ser defensivo con respecto a 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 finalizará la secuencia de comandos y se expulsará el estado.

Desarrollo local

Un componente importante de los service worker es almacenar recursos en caché a nivel local. Sin embargo, durante el desarrollo, este es exactamente lo opuesto a lo que deseas, en especial cuando las actualizaciones se realizan de forma diferida. Quieres que el trabajador del servidor esté instalado para poder depurar problemas o trabajar con otras APIs, como la sincronización en segundo plano o las notificaciones. En Chrome, puedes lograrlo con las Herramientas para desarrolladores de Chrome habilitando la casilla de verificación Omitir la red (panel Aplicación > panel Trabajadores de servicio) además de habilitar la casilla de verificación Inhabilitar caché en el panel Red para inhabilitar también la memoria caché. Con el fin de abarcar más navegadores, optamos por una solución diferente. Para ello, incluimos una marca que inhabilita el almacenamiento en caché en nuestro service worker, que está habilitada de forma predeterminada en las compilaciones de los desarrolladores. Esto garantiza que los desarrolladores siempre obtengan los 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 recursos.

Faro

Lighthouse proporciona una serie de herramientas de depuración útiles para las AWP. Analiza un sitio y genera informes que abarcan las AWP, el rendimiento, la accesibilidad, la SEO y otras prácticas recomendadas. Te recomendamos ejecutar Lighthouse en la integración continua para recibir una alerta si infringes uno de los criterios para ser una AWP. En realidad, esto nos ocurrió una vez, en la que el service worker no se instalaba ni nos dimos cuenta antes de un envío de producción. Esto se podría haber evitado con Lighthouse como parte de nuestra CI.

Adopta la entrega continua

Debido a que los service workers se pueden actualizar automáticamente, los usuarios no pueden limitar las actualizaciones. Esto reduce en gran medida la cantidad de clientes desactualizados existentes. Cuando el usuario abría la app, el service worker atiende el cliente anterior mientras descargaba el cliente nuevo de forma diferida. Una vez que se descargó el cliente nuevo, se le pedirá al usuario que actualice la página para acceder a las funciones nuevas. Incluso si el usuario ignoró esta solicitud, la próxima vez que actualizara la página recibiría la versión nueva del cliente. Como resultado, es bastante difícil para un usuario rechazar las actualizaciones de la misma manera que en las apps para iOS/Android.

Pudimos implementar cambios rotundos de backend con muy poco tiempo de migración para los clientes. Por lo general, asignamos un mes para que los usuarios realicen la actualización a clientes más nuevos antes de realizar cambios rotundos. Como la app se entregaba mientras estaba inactiva, los clientes más antiguos podían existir en el entorno si el usuario no la hubiera abierto por mucho tiempo. En iOS, los service workers se expulsan después de un par de semanas, por lo que este caso no ocurre. En Android, este problema se puede mitigar si no se publica el contenido mientras está inactivo o si se vence manualmente el contenido después de unas semanas. En la práctica, nunca encontramos problemas de clientes inactivos. Qué tan estricto quiere ser un equipo específico depende de su caso de uso específico, pero las AWP ofrecen mucha más flexibilidad que las apps para iOS/Android.

Obtén 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 permita autenticar las solicitudes a la API propias. En un service worker, las APIs síncronas como document.cookies no están disponibles. Siempre puedes enviar un mensaje a los clientes activos (con ventanas) desde el service worker para solicitar los valores de las cookies, aunque es posible que el service worker 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 repetía el valor de la cookie en el cliente. El service worker envió una solicitud de red a este extremo y leyó la respuesta para obtener los valores de las cookies.

Con el lanzamiento de la API de Cookie Store, esta solución alternativa 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 service worker puede usarla directamente.

Dificultades para service workers no generados

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

Un patrón de AWP común consiste en que un service worker instale todos los archivos estáticos de la aplicación durante su fase install, lo que permite a los clientes acceder a la caché de la API de Cache Storage directamente 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 se modificara de alguna manera cuando cambiaba un archivo almacenado en caché. Lo hicimos manualmente incorporando un hash del conjunto de archivos de recursos estáticos en la secuencia de comandos de nuestro service worker, de modo que cada actualización produzca un archivo JavaScript del service worker distinto. Las bibliotecas de service workers como Workbox automatizan este proceso.

Pruebas de unidades

Las APIs de service worker funcionan agregando 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 necesitas simular el activador del evento y el objeto del evento, esperar la devolución de llamada respondWith() y, luego, esperar la promesa antes de afirmar el resultado. Una manera más fácil de estructurar esto es delegar toda la implementación a otro archivo, que se prueba 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 del service worker principal de la forma más sencilla posible y dividimos la mayor parte de la implementación en otros módulos. Como esos archivos eran solo módulos JS estándar, se podrían realizar pruebas de unidades más fácilmente con bibliotecas de prueba 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 medios y problemas específicos de iOS. Si quieres obtener más información sobre cómo compilar una AWP en Google, visita nuestros perfiles de autores para obtener información sobre cómo comunicarte con nosotros: