Sebastian Gomez
Introducción a los Service Workers en Extensiones para Chrome
En este post exploraremos qué son los service workers y cómo funcionan dentro de las extensiones para Chrome. Vamos a implementar uno paso a paso, entenderemos por qué viven en un ámbito distinto al de la ventana y aprenderemos a comunicar el background con el pop up. Todo el código usa Manifest V3, que es la versión vigente y obligatoria en Chrome (Manifest V2 quedó retirado por completo en 2024).
¿Qué son los Service Workers?
Los service workers son trabajadores en segundo plano que se ejecutan de manera independiente del DOM (Document Object Model) de la página web o ventana. Son útiles para manejar tareas que no requieren interacción directa con la interfaz del usuario, como hacer solicitudes a APIs o gestionar eventos en segundo plano.
Una característica importante en Manifest V3 es que el service worker no vive para siempre: el navegador lo detiene cuando está inactivo y lo vuelve a despertar ante un evento. Por eso no debemos guardar estado solo en variables en memoria; conviene persistirlo, por ejemplo en chrome.storage.
Configuración del Service Worker
Primero necesitamos registrar un service worker en nuestro archivo manifest.json. Aquí hay un ejemplo básico:
{
"manifest_version": 3,
"name": "Mi Extensión",
"version": "0.0.1",
"background": {
"service_worker": "background.js"
}
}A continuación, creamos el archivo background.js con el siguiente contenido:
console.log('Hola desde el script de fondo');Verificar la actividad del Service Worker
Al cargar la extensión en Chrome, el service worker se registrará y ejecutará. Puedes verificar su actividad en la página chrome://extensions: activa el modo de desarrollador, carga tu extensión sin empaquetar y haz clic en el enlace "service worker" de tu tarjeta para abrir las herramientas de desarrollador asociadas y ver sus logs.
Diferencia entre el ámbito del Service Worker y el ámbito de la ventana
Es crucial entender que los service workers operan en un ámbito diferente al de la ventana (o DOM). Mientras que la ventana tiene acceso a la interfaz del usuario y puede manipular elementos del DOM, los service workers no tienen ese acceso.
Ámbito del Service Worker:
- Puede manejar tareas en segundo plano como solicitudes a APIs, manejo de eventos y más.
- No tiene acceso al DOM directamente, por lo que no puede manipular elementos de la interfaz de usuario.
Ámbito de la ventana:
- Incluye todo lo relacionado con la interfaz del usuario, como la manipulación del DOM y la interacción con la persona que usa la extensión.
- Puede comunicarse con el service worker mediante el envío de mensajes.
Implementación de ejemplos básicos
Vamos a implementar un ejemplo básico para entender mejor cómo funcionan estos conceptos.
Script de fondo (background.js):
console.log('Hola desde el script de fondo');Script de la ventana (popup.js):
console.log('Hola desde el script de la ventana');Archivo HTML del pop up (popup.html):
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Pop-up</title>
<script src="popup.js"></script>
</head>
<body>
<h1>Hola Mundo</h1>
</body>
</html>Actualizamos el manifest.json para declarar el pop up:
{
"manifest_version": 3,
"name": "Mi Extensión",
"version": "0.0.1",
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html"
}
}El archivo background.js se ejecuta en segundo plano y puede realizar tareas como manejar eventos del navegador o coordinar el estado de la extensión. Por otro lado, el pop up es una interfaz de usuario que aparece cuando la persona hace clic en el icono de la extensión en la barra de herramientas del navegador. La comunicación entre estos dos componentes es clave para muchas funcionalidades de las extensiones.
Envío de mensajes desde el pop up a background.js
Para enviar un mensaje desde el pop up al script de fondo, podemos usar la API chrome.runtime.sendMessage. Aquí hay un ejemplo desde popup.js:
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('sendMessage').addEventListener('click', function () {
chrome.runtime.sendMessage({ greeting: 'hola' }, function (response) {
console.log(response.farewell);
});
});
});Y en popup.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Pop-up</title>
</head>
<body>
<button id="sendMessage">Enviar Mensaje</button>
<script src="popup.js"></script>
</body>
</html>Hoy en día, chrome.runtime.sendMessage también devuelve una Promesa cuando no le pasas un callback, así que en código moderno puedes usar await en lugar de anidar funciones:
async function enviarSaludo() {
const response = await chrome.runtime.sendMessage({ greeting: 'hola' });
console.log(response.farewell);
}Nota. Las dos formas son válidas. Usa el callback si necesitas compatibilidad con código antiguo y la versión con await si prefieres un flujo asíncrono más limpio. No mezcles ambas en la misma llamada.
Recepción de mensajes en background.js
En background.js, escuchamos y respondemos los mensajes entrantes con chrome.runtime.onMessage.addListener:
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
console.log(
sender.tab
? 'Mensaje desde un content script: ' + sender.tab.url
: 'Mensaje desde la extensión',
);
if (request.greeting === 'hola') {
sendResponse({ farewell: 'adiós' });
}
});Nota muy importante. Este es el error número uno en la mensajería de Manifest V3. Si respondes de forma síncrona, como arriba, todo funciona. Pero si necesitas hacer trabajo asíncrono antes de llamar a sendResponse (por ejemplo, esperar a un fetch), debes devolver true; desde el listener para mantener abierto el canal de mensajes. Si lo olvidas, el canal se cierra antes de tu respuesta y quien envió el mensaje recibirá undefined.
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
if (request.greeting === 'hola') {
fetch('https://api.example.com/saludo')
.then((res) => res.json())
.then((data) => sendResponse({ farewell: data.farewell }));
return true; // mantiene vivo el canal para la respuesta asíncrona
}
});Envío de mensajes desde background.js al pop up
Aquí conviene aclarar un malentendido común. No existe una "referencia al pop up activo" que podamos obtener desde background.js: el pop up no es una ventana a la que tengamos un handle. Lo que hace chrome.runtime.sendMessage es enviar el mensaje al runtime de la extensión, y solo llegará si en ese momento el pop up está abierto y tiene registrado un listener con chrome.runtime.onMessage.
En la práctica esto significa que, si la persona no tiene el pop up abierto, el mensaje no tendrá quién lo reciba. Por eso muchas extensiones envían los datos en el otro sentido: el pop up pide la información al background cuando se abre.
En background.js:
function sendMessageToPopup() {
// Solo llegará si el pop-up está abierto y escuchando.
chrome.runtime.sendMessage({ greeting: 'hola' }, function (response) {
if (chrome.runtime.lastError) {
// No hay receptor: probablemente el pop-up está cerrado.
console.log('El pop-up no está abierto, nadie recibió el mensaje.');
return;
}
console.log(response.farewell);
});
}
// Puedes llamar a sendMessageToPopup cuando sea necesario.En popup.js, escuchamos los mensajes de la misma manera:
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
if (request.greeting === 'hola') {
sendResponse({ farewell: 'adiós' });
}
});Comunicación bidireccional con puertos
Para una comunicación más rica y bidireccional, podemos usar puertos (port) para mantener una conexión abierta entre el pop up y background.js. Aquí hay un ejemplo básico.
En popup.js:
const port = chrome.runtime.connect({ name: 'popup-background' });
port.postMessage({ greeting: 'hola' });
port.onMessage.addListener(function (msg) {
console.log('Mensaje recibido:', msg);
});En background.js:
chrome.runtime.onConnect.addListener(function (port) {
console.assert(port.name === 'popup-background');
port.onMessage.addListener(function (msg) {
console.log('Mensaje recibido:', msg);
if (msg.greeting === 'hola') {
port.postMessage({ farewell: 'adiós' });
}
});
});Los puertos son ideales cuando vas a intercambiar varios mensajes durante una sesión, ya que evitas abrir y cerrar un canal en cada envío.
Conclusión
Los service workers son una herramienta poderosa para gestionar tareas en segundo plano en tus extensiones para Chrome. Al entender la diferencia entre el ámbito del service worker y el de la ventana, y al manejar bien la mensajería, puedes crear extensiones más robustas y eficientes.
Ejercicios propuestos
- Crea una extensión mínima con Manifest V3 que registre un
background.jsy muestre un mensaje en consola al instalarse usandochrome.runtime.onInstalled. - Implementa un pop up con un botón que envíe un mensaje al background y muestre la respuesta. Hazlo primero con callback y luego reescríbelo con
await. - Modifica el listener del background para que responda de forma asíncrona (por ejemplo, tras un
fetch) y comprueba qué pasa con y sinreturn true;. - Sustituye los mensajes puntuales por una conexión con
porty envía al menos tres mensajes en ambos sentidos durante la misma sesión.
Resumen en 3 puntos
- En Manifest V3, el background es un service worker que se ejecuta en segundo plano, sin acceso al DOM y sin vida garantizada, así que conviene persistir el estado.
- La comunicación entre componentes se hace con mensajes (
runtime.sendMessage/onMessage) o con puertos (connect/onConnect); recuerdareturn true;cuando la respuesta sea asíncrona. - No existe un handle directo al pop up: un mensaje del background solo llega si el pop up está abierto y escuchando.
Eso es todo, espero que este post te sea de utilidad y lo puedas aplicar a algún proyecto que tengas en mente. Déjame un comentario si te sirvió, si quieres añadir alguna opinión o si tienes alguna duda. Y recuerda que si te gustó, también puedes compartirlo usando los links a las redes sociales aquí abajo. ¡Buena suerte y a seguir construyendo extensiones!
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.