Migra a un service worker

Reemplaza las páginas de fondo o de eventos por un service worker.

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

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

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

Diferencias entre las secuencias de comandos en segundo plano y los service workers de extensión

En algunos contextos, verás service workers de extensión llamados “secuencias de comandos en segundo plano”. Aunque los service workers de extensión se ejecutan en segundo plano, llamarlos a secuencias de comandos en segundo plano es un poco engañoso porque implica capacidades idénticas. Las diferencias se describen a continuación.

Cambios desde páginas en segundo plano

Los service workers tienen varias diferencias con las páginas de fondo.

  • 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 interactuar y comunicarse con otros contextos a través de la interfaz de clientes.

Cambios que deberás realizar

Deberás realizar algunos ajustes de 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, el modo en que se especifica un service worker en el archivo de manifiesto es diferente de cómo se especifican las secuencias de comandos en segundo plano. Además, tenga en cuenta lo siguiente:

  • Como no pueden acceder al DOM o a la interfaz de window, deberás mover esas llamadas a una API diferente o a un documento fuera de pantalla.
  • Los objetos de escucha de eventos no deben registrarse en respuesta a promesas que se muestren ni dentro de devoluciones de llamada de eventos.
  • Dado que no son retrocompatibles con XMLHttpRequest(), deberás reemplazar las llamadas a esta interfaz por llamadas a fetch().
  • Dado que estas finalizan cuando no se usan, deberás conservar los estados de la aplicación en lugar de depender de variables globales. Los service workers finalizados también pueden finalizar los cronómetros antes de que se completen. Deberás reemplazarlas por alarmas.

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

Actualiza el campo "fondo" en el manifiesto

En Manifest V3, las páginas de fondo se reemplazan por un service worker. Los cambios en el manifiesto se enumeran a continuación.

  • Reemplaza "background.scripts" por "background.service_worker" en manifest.json. Ten en cuenta que el campo "service_worker" toma una string, no un array de strings.
  • 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 string. Solo necesitarás el campo "type" si utilizas módulos de 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

Traslada el DOM y las llamadas de las ventanas a un documento fuera de la pantalla

Algunas extensiones necesitan acceso al DOM y los objetos de la ventana sin abrir visualmente una ventana o pestaña nueva. La API fuera de pantalla admite estos casos de uso, ya que permite abrir y cerrar documentos no mostrados empaquetados con extensión sin interrumpir la experiencia del usuario. A excepción del paso de mensajes, los documentos fuera de pantalla no comparten APIs con otros contextos de extensiones, sino que funcionan como páginas web completas para que las extensiones interactúen con ellas.

Para usar la API fuera de pantalla, 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 habrías ejecutado en una secuencia de comandos en segundo plano. Por ejemplo, puedes copiar el texto seleccionado en la página de alojamiento.

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

La comunicación entre los documentos fuera de pantalla y los service workers de la extensión utiliza el transferencia de mensajes.

Convierte localStorage en otro tipo

La interfaz Storage de la plataforma web (accesible desde window.localStorage) no se puede usar en un service worker. Para abordar esto, sigue uno de estos dos pasos. Primero, puedes reemplazarlo con llamadas a otro mecanismo de almacenamiento. El espacio de nombres chrome.storage.local se usará para la mayoría de los casos de uso, pero hay otras opciones disponibles.

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

  1. Crea un documento fuera de pantalla con una rutina de conversión y un controlador runtime.onMessage.
  2. Agrega una rutina de conversión al documento fuera de pantalla.
  3. En el service worker de extensión, busca tus datos en chrome.storage.
  4. Si no se encuentran tus datos, crea un documento fuera de 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 en el funcionamiento de las APIs de almacenamiento web en las extensiones. Obtén más información en Almacenamiento y cookies.

Registra objetos de escucha de forma síncrona

El registro de un objeto de escucha de forma asíncrona (por ejemplo, dentro de una promesa o devolución de llamada) no garantiza que 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, ya que la página se ejecuta constantemente y nunca se reinicia. En Manifest V3, el service worker se reiniciará 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 la secuencia de comandos. Esto garantiza que Chrome pueda buscar e invocar de inmediato el controlador de clics de tu acción, incluso si la 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 });
});

Cómo reemplazar XMLHttpRequest() con fetch() global

No se puede llamar a XMLHttpRequest() desde un service worker, una extensión ni otro tipo de extensión. Reemplaza las llamadas de la secuencia de comandos en segundo plano por 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 debido a que se eliminó el contexto anterior. Para evitarlo, usa las APIs de Storage como fuente de información. En un ejemplo, se muestra cómo hacerlo.

En el siguiente ejemplo, se usa una variable global para almacenar un nombre. En un service worker, esta variable podría restablecerse 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 });
});

Para 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 });
});

Convierte temporizadores en alarmas

Es común usar operaciones retrasadas o periódicas 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 alarmas deben registrarse en el nivel superior de la 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(),
  });
});

Mantén activo el service worker

Por definición, los service workers son controlados por eventos y finalizan en inactividad. 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 del ciclo de vida de los service worker. En casos excepcionales, pueden requerirse medidas adicionales para garantizar que un service worker permanezca vivo durante más tiempo.

Mantén 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, el service worker podría cerrarse durante la operación. Los siguientes son algunos ejemplos:

  • Una solicitud de fetch() 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 la vida útil 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 está reservado solo para casos excepcionales y que, en la mayoría de las situaciones, suele haber una forma mejor, idiomática de plataforma, de lograr el mismo resultado.

En el siguiente ejemplo, se muestra una función auxiliar waitUntil() que mantiene activo el 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());

Mantener activo un service worker de forma continua

En casos excepcionales, es necesario extender la vida útil de forma indefinida. Identificamos a las empresas y la educación como los casos de uso más importantes y permitimos esto específicamente, pero no lo admitimos en general. En estas circunstancias excepcionales, se puede mantener activo un service worker mediante llamadas periódicas 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 está permitido 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 el 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'];
}