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

Qué aprendió el equipo de Boletín 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 Boletín de Google mientras compilaba una AWP externa. En estas publicaciones, compartiremos algunos de los desafíos que enfrentamos, los enfoques que adoptamos para superarlos y consejos generales para evitar dificultades. De ninguna manera se trata de una descripción general completa de las AWP. El objetivo es compartir los hallazgos de la experiencia de nuestro equipo.

En esta primera publicación, veremos un poco de información general y, luego, 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 compilar 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. 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 funcionaba 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 rotundos en el backend con un tiempo de migración muy corto 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.
  • Se eliminó la fricción que implicaba instalar una aplicación.

Nuestro marco de trabajo

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

Qué aprendimos sobre los service workers

No puedes tener una AWP sin un service worker. Los service workers te brindan 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 cierta complejidad, descubrimos que sus beneficios superaban esta complejidad adicional.

Generarla si puedes

Evita escribir una secuencia de comandos del service worker de forma manual. La escritura manual de service workers requiere administrar manualmente los recursos almacenados en caché y reescribir la lógica, que es común en la mayoría de las bibliotecas de service workers, 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 aprendimos a continuación reflejará eso. Consulta Errores de service workers no generados para obtener más información.

No todas las bibliotecas son compatibles con service worker

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 esenciales que necesitas para tu aplicación sean compatibles con service workers. Para esta AWP en particular, queríamos usar gapi.js para la autenticación, pero no pudimos hacerlo porque no admitía service workers. Siempre que sea posible, los autores de las bibliotecas también deben reducir o quitar las suposiciones innecesarias sobre el contexto de JavaScript para admitir casos de uso de service worker, como evitar las APIs incompatibles con el service worker y evitar el estado global.

Evita acceder a IndexedDB durante la inicialización

No leas IndexedDB cuando inicialices la secuencia de comandos del service worker. De lo contrario, podrías encontrarte en una situación no deseada:

  1. El usuario tiene una aplicación 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, que activa la descarga de un nuevo service worker
  4. Un nuevo service worker lee de IDB antes de registrar el controlador de eventos install, lo que activa un ciclo de actualización del IDB para que pase de N a N + 1.
  5. Dado que el usuario tiene un cliente anterior con la versión N, el proceso de actualización del service worker 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, se invalidó la caché durante la instalación del service worker, por lo que, si nunca lo instaló, los usuarios no recibirán la app actualizada.

Haz que sea resiliente

Aunque las secuencias de comandos del service worker se ejecutan en segundo plano, también pueden finalizarse en cualquier momento, incluso cuando están en medio de operaciones de E/S (red, IDB, etcétera). 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 guardaban en IDB, nuestra solución para las cargas parciales interrumpidas consistía en aprovechar el sistema reanudable de nuestra biblioteca de carga interna, guardar la URL de carga reanudable en IDB antes de subir 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 IDB para indicar en qué parte del proceso estábamos para cada registro.

No depender del estado global

Debido a que los service workers existen en un contexto diferente, muchos de los símbolos que podría esperar que existan no están presentes. Gran parte de nuestro código se ejecutó en un contexto window y en un contexto de service worker (como registro, marcas, 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 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 expulsará el estado.

Desarrollo local

Uno de los componentes principales de los service workers 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. De todos modos, querrás instalar el trabajador del servidor para poder 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 service worker, que está habilitado de forma predeterminada en las compilaciones de desarrollador. 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 una serie de herramientas de depuración útiles para las AWP. Analiza un sitio y genera informes sobre las AWP, el rendimiento, la accesibilidad, la 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 ocurrió una vez, cuando el service worker no se instalaba y no nos habíamos dado cuenta antes del envío de producción. Tener Lighthouse como parte de nuestra CI habría evitado eso.

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 exterior. 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 haría en las apps para iOS y Android.

Pudimos implementar cambios rotundos en el backend con un tiempo de migración muy corto para los clientes. Por lo general, los usuarios tardan un mes en actualizarse a clientes más nuevos antes de realizar cambios rotundos. 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 este caso no ocurre. En Android, este problema podría mitigarse 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. Lo estricto que quiere ser un equipo determinado depende de su caso de uso específico, pero las AWP proporcionan mucha más flexibilidad que las apps para iOS/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 este caso, tuvimos que acceder a los valores de las cookies para generar un token que autentique las solicitudes propias a la API. 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 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.

Errores de 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 es que un service worker instale todos los archivos estáticos de aplicaciones durante su fase install, lo que permite a los clientes acceder 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 la secuencia de comandos de nuestro service worker, por lo que cada actualización produjo un archivo JavaScript distinto de service worker. Las bibliotecas de service worker, 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 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 prueba con más facilidad.

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

Debido a las dificultades de la prueba de unidades de una secuencia de comandos de service worker, mantuvimos la secuencia de comandos del service worker principal lo más simple posible y dividimos la mayor parte de la implementación en otros módulos. Como esos archivos eran solo módulos de JS estándar, se podían realizar pruebas de unidades con mayor facilidad mediante las 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 los problemas específicos de iOS. Si quieres preguntarnos más sobre la compilación de una AWP en Google, visita nuestros perfiles de autores para averiguar cómo contactarnos: