Migra a un service worker

Reemplaza las páginas de eventos o de segundo plano con un service worker

Un service worker reemplaza la página de eventos o de segundo plano de la extensión para garantizar que el código de segundo plano permanezca fuera del subproceso principal. Esto permite que las extensiones se ejecuten solo cuando sea necesario, lo que ahorra recursos.

Las páginas en segundo plano han sido un componente fundamental de las extensiones desde su introducción. En pocas palabras, las páginas en segundo plano proporcionan un entorno que existe independientemente de cualquier otra ventana o pestaña. Esto permite que las extensiones observen los eventos y actúen en respuesta a ellos.

En esta página, se describen las tareas para convertir páginas en segundo plano en Service Workers de extensiones. Para obtener más información sobre los service workers de extensiones en general, consulta el instructivo Cómo controlar eventos con service workers y la sección Acerca de los service workers de extensiones.

Diferencias entre los scripts en segundo plano y los service workers de extensiones

En algunos contextos, verás service workers de extensiones llamados "secuencias de comandos en segundo plano". Si bien los service workers de extensiones se ejecutan en segundo plano, llamarlos secuencias de comandos en segundo plano es algo engañoso, ya que implica que tienen las mismas capacidades. Las diferencias se describen a continuación.

Cambios en las páginas en segundo plano

Los service workers tienen varias diferencias con las páginas en segundo plano.

  • Funcionan fuera del subproceso principal, lo que significa que no interfieren con el contenido de la extensión.
  • Tienen capacidades especiales, como interceptar eventos de recuperación en el origen de la extensión, como los de una ventana emergente de la barra de herramientas.
  • Pueden comunicarse e interactuar con otros contextos a través de la interfaz de Clients.

Cambios que deberás realizar

Deberás realizar algunos ajustes en el código para tener en cuenta las diferencias entre el funcionamiento de las secuencias de comandos en segundo plano y los service workers. Para empezar, la forma en que se especifica un service worker en el archivo de manifiesto es diferente de la forma en que se especifican los secuencias de comandos en segundo plano. Además:

  • Como no pueden acceder al DOM ni a la interfaz de window, deberás trasladar esas llamadas a otra API o a un documento fuera de la pantalla.
  • Los objetos de escucha de eventos no se deben registrar en respuesta a promesas devueltas ni dentro de devoluciones de llamada de eventos.
  • Como no son retrocompatibles con XMLHttpRequest(), deberás reemplazar las llamadas a esta interfaz por llamadas a fetch().
  • Como finalizan cuando no están en uso, deberás conservar los estados de la aplicación en lugar de depender de variables globales. La finalización de los service workers también puede detener los temporizadores antes de que se completen. Deberás reemplazarlos por alarmas.

En esta página, se describen estas tareas en detalle.

Actualiza el campo "background" en el manifiesto

En Manifest V3, las páginas en segundo plano se reemplazan por un service worker. A continuación, se indican los cambios en el manifiesto.

  • Reemplaza "background.scripts" por "background.service_worker" en manifest.json. Ten en cuenta que el campo "service_worker" toma una cadena, no un array de cadenas.
  • Quita "background.persistent" de manifest.json.
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

El campo "service_worker" toma una sola cadena. Solo necesitarás el campo "type" si usas módulos ES (con la palabra clave import). Su valor siempre será "module". Para obtener más información, consulta Conceptos básicos del service worker de extensiones.

Mueve las llamadas de DOM y de ventana a un documento fuera de pantalla

Algunas extensiones necesitan acceder a los objetos DOM y de ventana sin abrir visualmente una nueva ventana o pestaña. La API de Offscreen admite estos casos de uso abriendo y cerrando documentos no mostrados empaquetados con la extensión, sin interrumpir la experiencia del usuario. A excepción del envío de mensajes, los documentos fuera de pantalla no comparten APIs con otros contextos de extensión, sino que funcionan como páginas web completas con las que las extensiones pueden interactuar.

Para usar la API de Offscreen, crea un documento fuera de pantalla desde el service worker.

chrome.offscreen.createDocument({
  url: chrome.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

En el documento fuera de pantalla, realiza cualquier acción que antes hubieras ejecutado en una secuencia de comandos en segundo plano. Por ejemplo, puedes copiar el texto seleccionado en la página del host.

let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');

Comunicarse entre documentos fuera de pantalla y trabajadores de servicio de extensiones con el paso de mensajes

Cómo convertir localStorage a otro tipo

No se puede usar la interfaz Storage de la plataforma web (a la que se puede acceder desde window.localStorage) en un service worker. Para solucionar este problema, realiza una de las siguientes acciones. Primero, puedes reemplazarlo por llamadas a otro mecanismo de almacenamiento. El espacio de nombres chrome.storage.local abarcará la mayoría de los casos de uso, pero hay otras opciones disponibles.

También puedes mover sus llamadas a un documento fuera de la pantalla. Por ejemplo, para migrar datos almacenados anteriormente en localStorage a otro mecanismo, haz lo siguiente:

  1. Crea un documento fuera de pantalla con una rutina de conversión y un controlador de runtime.onMessage.
  2. Agrega una rutina de conversión al documento fuera de pantalla.
  3. En la verificación del service worker de la extensión, busca chrome.storage para tus datos.
  4. Si no se encuentran tus datos, crea un documento fuera de la pantalla y llama a runtime.sendMessage() para iniciar la rutina de conversión.
  5. En el controlador runtime.onMessage que agregaste al documento fuera de pantalla, llama a la rutina de conversión.

También hay algunos matices sobre cómo funcionan las APIs de almacenamiento web en las extensiones. Obtén más información en Almacenamiento y cookies.

Cómo registrar objetos de escucha de forma síncrona

No se garantiza que el registro de un objeto de escucha de forma asíncrona (por ejemplo, dentro de una promesa o devolución de llamada) funcione en Manifest V3. Considera el siguiente código.

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.browserAction.setBadgeText({ text: badgeText });
  chrome.browserAction.onClicked.addListener(handleActionClick);
});

Esto funciona con una página en segundo plano persistente porque la página se ejecuta constantemente y nunca se reinicializa. En Manifest V3, el Service Worker se reinicializará cuando se envíe el evento. Esto significa que, cuando se active el evento, los objetos de escucha no se registrarán (ya que se agregan de forma asíncrona) y se perderá el evento.

En su lugar, mueve el registro del objeto de escucha de eventos al nivel superior de tu secuencia de comandos. Esto garantiza que Chrome pueda encontrar e invocar de inmediato el controlador de clics de tu acción, incluso si tu extensión no terminó de ejecutar su lógica de inicio.

chrome.action.onClicked.addListener(handleActionClick);

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.action.setBadgeText({ text: badgeText });
});

Reemplaza XMLHttpRequest() por fetch() global

No se puede llamar a XMLHttpRequest() desde un service worker, una extensión o de otra manera. Reemplaza las llamadas de tu secuencia de comandos en segundo plano a XMLHttpRequest() por llamadas a fetch() global.

XMLHttpRequest()
const xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState);

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState);

xhr.onload = () => {
    console.log('DONE', xhr.readyState);
};
xhr.send(null);
fetch()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

Estados persistentes

Los service workers son efímeros, lo que significa que es probable que se inicien, ejecuten y finalicen varias veces durante la sesión del navegador de un usuario. También significa que los datos no están disponibles de inmediato en las variables globales, ya que se interrumpió el contexto anterior. Para evitar esto, usa las APIs de almacenamiento como fuente de información. En un ejemplo, se mostrará cómo hacerlo.

En el siguiente ejemplo, se usa una variable global para almacenar un nombre. En un service worker, esta variable se podría restablecer varias veces durante la sesión del navegador de un usuario.

Secuencia de comandos en segundo plano de Manifest V2
let savedName = undefined;

chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    savedName = name;
  }
});

chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.sendMessage(tab.id, { name: savedName });
});

En Manifest V3, reemplaza la variable global por una llamada a la API de Storage.

Service worker de Manifest V3
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

chrome.action.onClicked.addListener(async (tab) => {
  const { name } = await chrome.storage.local.get(["name"]);
  chrome.tabs.sendMessage(tab.id, { name });
});

Cómo convertir temporizadores en alarmas

Es común usar operaciones periódicas o retrasadas con los métodos setTimeout() o setInterval(). Sin embargo, estas APIs pueden fallar en los service workers, ya que los temporizadores se cancelan cada vez que se finaliza el service worker.

Secuencia de comandos en segundo plano de Manifest V2
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

En su lugar, usa la API de Alarms. Al igual que con otros objetos de escucha, los objetos de escucha de alarma deben registrarse en el nivel superior de tu secuencia de comandos.

Service worker de Manifest V3
async function startAlarm(name, duration) {
  await chrome.alarms.create(name, { delayInMinutes: 3 });
}

chrome.alarms.onAlarm.addListener(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
});

Cómo mantener activo el service worker

Los service workers se basan en eventos por definición y se detendrán si no hay actividad. De esta manera, Chrome puede optimizar el rendimiento y el consumo de memoria de tu extensión. Obtén más información en nuestra documentación sobre el ciclo de vida de los Service Workers. En casos excepcionales, es posible que se requieran medidas adicionales para garantizar que un service worker permanezca activo durante más tiempo.

Cómo mantener activo un service worker hasta que finalice una operación de larga duración

Durante las operaciones de larga duración del service worker que no llaman a las APIs de extensión, es posible que el service worker se cierre a mitad de la operación. Los siguientes son algunos ejemplos:

  • Una solicitud de fetch() que podría tardar más de cinco minutos (p. ej., una descarga grande en una conexión potencialmente deficiente)
  • Un cálculo asíncrono complejo que tarda más de 30 segundos.

Para extender el ciclo de vida del service worker en estos casos, puedes llamar periódicamente a una API de extensión trivial para restablecer el contador de tiempo de espera. Ten en cuenta que esto solo se reserva para casos excepcionales y, en la mayoría de las situaciones, suele haber una mejor manera idiomática de la plataforma para lograr el mismo resultado.

En el siguiente ejemplo, se muestra una función auxiliar waitUntil() que mantiene activo tu service worker hasta que se resuelve una promesa determinada:

async function waitUntil(promise) {
  const keepAlive = setInterval(chrome.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

Mantén un service worker activo de forma continua

En casos excepcionales, es necesario extender el ciclo de vida de forma indefinida. Identificamos que los casos de uso más importantes son los empresariales y educativos, y permitimos específicamente este uso en esos casos, pero no lo admitimos en general. En estas circunstancias excepcionales, se puede mantener activo un service worker llamando periódicamente a una API de extensión trivial. Es importante tener en cuenta que esta recomendación solo se aplica a las extensiones que se ejecutan en dispositivos administrados para casos de uso empresariales o educativos. No se permite en otros casos, y el equipo de extensiones de Chrome se reserva el derecho de tomar medidas contra esas extensiones en el futuro.

Usa el siguiente fragmento de código para mantener activo tu service worker:

/**
 * Tracks when a service worker was last alive and extends the service worker
 * lifetime by writing the current time to extension storage every 20 seconds.
 * You should still prepare for unexpected termination - for example, if the
 * extension process crashes or your extension is manually stopped at
 * chrome://serviceworker-internals. 
 */
let heartbeatInterval;

async function runHeartbeat() {
  await chrome.storage.local.set({ 'last-heartbeat': new Date().getTime() });
}

/**
 * Starts the heartbeat interval which keeps the service worker alive. Call
 * this sparingly when you are doing work which requires persistence, and call
 * stopHeartbeat once that work is complete.
 */
async function startHeartbeat() {
  // Run the heartbeat once at service worker startup.
  runHeartbeat().then(() => {
    // Then again every 20 seconds.
    heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
  });
}

async function stopHeartbeat() {
  clearInterval(heartbeatInterval);
}

/**
 * Returns the last heartbeat stored in extension storage, or undefined if
 * the heartbeat has never run before.
 */
async function getLastHeartbeat() {
  return (await chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
}