Módulos de ES en service workers

Una alternativa moderna a importScripts().

Fondo

Los módulos de ES son los favoritos de los desarrolladores desde hace un tiempo. Además de muchos otros beneficios, ofrecen la promesa de un formato de módulo universal en el que el código compartido se puede lanzar una vez y ejecutar en navegadores y en entornos de ejecución alternativos, como Node.js. Si bien todos los navegadores modernos ofrecen cierta compatibilidad con los módulos de ES, no todos ofrecen compatibilidad en todos los lugares en los que se puede ejecutar el código. Específicamente, la compatibilidad para importar módulos de ES dentro del service worker de un navegador recién comienza a estar disponible de forma más generalizada.

En este artículo, se detalla el estado actual de la compatibilidad con módulos de ES en los service workers en los navegadores habituales, junto con algunas trampas que se deben evitar y las prácticas recomendadas para enviar código de service worker retrocompatible.

Casos de uso

El caso de uso ideal para los módulos de ES dentro de los service workers es cargar una biblioteca moderna o un código de configuración que se comparta con otros entornos de ejecución que admitan módulos de ES.

Intentar compartir código de esta manera antes de los módulos de ES implicaba usar formatos de módulo "universales" más antiguos, como UMD, que incluyen código estándar innecesario, y escribir código que realizaba cambios en variables expuestas de forma global.

Las secuencias de comandos importadas a través de módulos de ES pueden activar el flujo de actualización del service worker si cambia su contenido, lo que coincide con el comportamiento de importScripts().

Limitaciones actuales

Solo importaciones estáticas

Los módulos de ES se pueden importar de dos maneras: estáticamente, con la sintaxis import ... from '...', o dinámicamente, con el método import(). Dentro de un service worker, actualmente solo se admite la sintaxis estática.

Esta limitación es similar a una restricción parecida que se aplica al uso de importScripts(). Las llamadas dinámicas a importScripts() no funcionan dentro de un service worker, y todas las llamadas a importScripts(), que son inherentemente síncronas, deben completarse antes de que el service worker complete su fase install. Esta restricción garantiza que el navegador conozca todo el código de JavaScript necesario para la implementación de un service worker durante la instalación y pueda almacenarlo en caché de forma implícita.

Con el tiempo, es posible que se quite esta restricción y que se permitan las importaciones dinámicas de módulos de ES. Por ahora, asegúrate de usar solo la sintaxis estática dentro de un service worker.

¿Qué pasa con los demás trabajadores?

La compatibilidad con los módulos de ES en los trabajadores "dedicados", es decir, aquellos construidos con new Worker('...', {type: 'module'}), está más extendida y se admite en Chrome y Edge desde la versión 80, así como en las versiones recientes de Safari. Las importaciones de módulos de ES estáticas y dinámicas son compatibles con los trabajadores dedicados.

Chrome y Edge admiten módulos de ES en workers compartidos desde la versión 83, pero ningún otro navegador ofrece compatibilidad en este momento.

No se admiten los mapas de importación.

Los mapas de importación permiten que los entornos de ejecución reescriban los especificadores de módulos para, por ejemplo, anteponer la URL de una CDN preferida desde la que se pueden cargar los módulos de ES.

Si bien Chrome y Edge versión 89 y posteriores admiten mapas de importación, actualmente no se pueden usar con service workers.

Navegadores compatibles

Los módulos de ES en los service workers son compatibles con Chrome y Edge a partir de la versión 91.

Safari agregó compatibilidad en la versión preliminar de tecnología 122, y los desarrolladores deberían ver esta funcionalidad en la versión estable de Safari en el futuro.

Ejemplo de código

Este es un ejemplo básico de cómo usar un módulo de ES compartido en el contexto de window de una app web y, al mismo tiempo, registrar un service worker que use el mismo módulo de ES:

// Inside config.js:
export const cacheName = 'my-cache';
// Inside your web app:
<script type="module">
  import {cacheName} from './config.js';
  // Do something with cacheName.

  await navigator.serviceWorker.register('es-module-sw.js', {
    type: 'module',
  });
</script>
// Inside es-module-sw.js:
import {cacheName} from './config.js';

self.addEventListener('install', (event) => {
  event.waitUntil((async () => {
    const cache = await caches.open(cacheName);
    // ...
  })());
});

Retrocompatibilidad

El ejemplo anterior funcionaría bien si todos los navegadores admitieran módulos de ES en los service workers, pero, al momento de escribir este artículo, no es así.

Para adaptar los navegadores que no tienen compatibilidad integrada, puedes ejecutar la secuencia de comandos de tu service worker a través de un empaquetador compatible con módulos de ES para crear un service worker que incluya todo el código del módulo intercalado y que funcione en navegadores más antiguos. Como alternativa, si los módulos que intentas importar ya están disponibles agrupados en formatos IIFE o UMD, puedes importarlos con importScripts().

Una vez que tengas dos versiones de tu service worker disponibles (una que use módulos de ES y otra que no), deberás detectar qué admite el navegador actual y registrar la secuencia de comandos del service worker correspondiente. Las prácticas recomendadas para detectar la compatibilidad están en constante cambio, pero puedes seguir el debate en este problema de GitHub para obtener recomendaciones.