Controla eventos con service workers

Instructivo en el que se abordan los conceptos de los service workers de extensiones

Descripción general

En este instructivo, se proporciona una introducción a los service workers de extensiones de Chrome. Como parte de este compilarás una extensión que permitirá a los usuarios navegar rápidamente a la referencia de la API de Chrome usando el cuadro multifunción. Aprenderás a hacer lo siguiente:

  • Registra tu service worker y, luego, importa módulos.
  • Depura tu service worker de extensión.
  • Administra el estado y controla eventos.
  • Activa eventos periódicos.
  • Comunicarse con los guiones de contenido

Antes de comenzar

En esta guía, se da por sentado que tienes experiencia básica en desarrollo web. Te recomendamos que revises Extensiones 101 y Hello World para obtener una introducción al desarrollo de extensiones.

Compila la extensión

Comienza por crear un directorio nuevo llamado quick-api-reference para contener los archivos de extensión o descarga el código fuente de nuestro repositorio de muestras de GitHub.

Paso 1: Registra el service worker

Crea el archivo de manifiesto en la raíz del proyecto y agrega el siguiente código:

manifest.json:

{
  "manifest_version": 3,
  "name": "Open extension API reference",
  "version": "1.0.0",
  "icons": {
    "16": "images/icon-16.png",
    "128": "images/icon-128.png"
  },
  "background": {
    "service_worker": "service-worker.js"
  }
}

Las extensiones registran el service worker en el manifiesto, que solo admite un único archivo JavaScript. No es necesario llamar a navigator.serviceWorker.register(), como lo harías en una página web.

Crea una carpeta images y, luego, descarga los íconos en ella.

Consulta los primeros pasos del instructivo de Tiempo de lectura para obtener más información sobre los metadatos y los íconos de las extensiones en el manifiesto.

Paso 2: Importa varios módulos de service worker

Nuestro trabajador de servicio implementa dos funciones. Para mejorar la capacidad de mantenimiento, implementaremos cada función en un módulo independiente. Primero, debemos declarar el service worker como un módulo de ES en nuestro manifiesto, lo que nos permite importar módulos en nuestro service worker:

manifest.json:

{
 "background": {
    "service_worker": "service-worker.js",
    "type": "module"
  },
}

Crea el archivo service-worker.js e importa dos módulos:

import './sw-omnibox.js';
import './sw-tips.js';

Crea estos archivos y agrega un registro de la consola a cada uno.

sw-omnibox.js:

console.log("sw-omnibox.js");

sw-tips.js:

console.log("sw-tips.js");

Consulta Importar secuencias de comandos para conocer otras formas de importar varios archivos en un service worker.

Opcional: Cómo depurar el servicio trabajador

Explicaré cómo encontrar los registros del service worker y saber cuándo se ha finalizado. Primero, siga las instrucciones que se indican en el artículo Cómo cargar una extensión sin empaquetar.

Después de 30 segundos, verás “service worker (inactive)” lo que significa que el service worker finalizó. Haz clic en el "service worker (inactivo)" para inspeccionarlo. En la siguiente animación, se muestra esto.

¿Notaste que la inspección del service worker lo activó? Si abres el service worker en las herramientas para desarrolladores, se mantendrá activo. Para asegurarte de que la extensión se comporte correctamente cuando se cierra el service worker, recuerda cerrar Herramientas para desarrolladores.

Ahora, divida la extensión para saber dónde localizar errores. Una forma de hacerlo es borrar ".js" a partir de la importación de './sw-omnibox.js' en el archivo service-worker.js. Chrome no podrá registrar el service worker.

Regresa a chrome://extensions y actualiza la extensión. Verás dos errores:

Service worker registration failed. Status code: 3.

An unknown error occurred when fetching the script.

Consulta Cómo depurar extensiones para obtener más formas de depurar el trabajador del servicio de la extensión.

Paso 4: Inicializa el estado

Chrome cerrará los trabajadores de servicio si no son necesarios. Usamos la API de chrome.storage para conservar el estado en todas las sesiones del trabajador de servicio. Para el acceso al almacenamiento, debemos solicitar permiso en el manifiesto:

manifest.json:

{
  ...
  "permissions": ["storage"],
}

Primero, guarda las sugerencias predeterminadas en el almacenamiento. Podemos inicializar el estado cuando se instala la extensión por primera vez escuchando el evento runtime.onInstalled():

sw-omnibox.js:

...
// Save default API suggestions
chrome.runtime.onInstalled.addListener(({ reason }) => {
  if (reason === 'install') {
    chrome.storage.local.set({
      apiSuggestions: ['tabs', 'storage', 'scripting']
    });
  }
});

Los service workers no tienen acceso directo al objeto window y, por lo tanto, no pueden usarlo window.localStorage para almacenar valores. Además, los trabajadores del servicio son entornos de ejecución de corta duración, ya que se cancelan de forma reiterada durante la sesión del navegador de un usuario, lo que los hace incompatibles con las variables globales. En su lugar, usa chrome.storage.local, que almacena datos en la máquina local.

Consulta Cómo conservar datos en lugar de usar variables globales para obtener información sobre otras opciones de almacenamiento para los trabajadores del servicio de extensión.

Paso 5: Registra tus eventos

Todos los objetos de escucha de eventos deben registrarse de forma estática en el alcance global del service worker. En otras palabras, los objetos de escucha de eventos no deben estar anidados en funciones asíncronas. De esta manera, Chrome puede garantizar que se restablezcan todos los controladores de eventos en caso de que se reinicie un service worker.

En este ejemplo, usaremos la API de chrome.omnibox, pero primero debemos declarar el activador de palabras clave de la barra de búsqueda en el manifiesto:

manifest.json:

{
  ...
  "minimum_chrome_version": "102",
  "omnibox": {
    "keyword": "api"
  },
}

Ahora, registra los objetos de escucha de eventos del cuadro multifunción en el nivel superior de la secuencia de comandos. Cuando el usuario ingrese la palabra clave del cuadro multifunción (api) en la barra de direcciones seguida de una pestaña o un espacio, Chrome mostrará una lista de sugerencias basada en las palabras clave almacenadas. El evento onInputChanged(), que toma la entrada del usuario actual y un objeto suggestResult, es responsable de propagar estas sugerencias.

sw-omnibox.js:

...
const URL_CHROME_EXTENSIONS_DOC =
  'https://developer.chrome.com/docs/extensions/reference/';
const NUMBER_OF_PREVIOUS_SEARCHES = 4;

// Display the suggestions after user starts typing
chrome.omnibox.onInputChanged.addListener(async (input, suggest) => {
  await chrome.omnibox.setDefaultSuggestion({
    description: 'Enter a Chrome API or choose from past searches'
  });
  const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
  const suggestions = apiSuggestions.map((api) => {
    return { content: api, description: `Open chrome.${api} API` };
  });
  suggest(suggestions);
});

Una vez que el usuario seleccione una sugerencia, onInputEntered() abrirá la página de referencia de la API de Chrome correspondiente.

sw-omnibox.js:

...
// Open the reference page of the chosen API
chrome.omnibox.onInputEntered.addListener((input) => {
  chrome.tabs.create({ url: URL_CHROME_EXTENSIONS_DOC + input });
  // Save the latest keyword
  updateHistory(input);
});

La función updateHistory() toma la entrada del cuadro multifunción y la guarda en storage.local. De esta manera, el término de búsqueda más reciente se puede usar más adelante como una sugerencia del cuadro multifunción.

sw-omnibox.js:

...
async function updateHistory(input) {
  const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
  apiSuggestions.unshift(input);
  apiSuggestions.splice(NUMBER_OF_PREVIOUS_SEARCHES);
  return chrome.storage.local.set({ apiSuggestions });
}

Paso 6: Configura un evento recurrente

Los métodos setTimeout() o setInterval() suelen usarse para realizar tareas demoradas o periódicas. Sin embargo, estas APIs pueden fallar porque el programador cancelará los cronómetros cuando el servicio trabajador finaliza. En su lugar, las extensiones pueden usar la API de chrome.alarms.

Para comenzar, solicita el permiso "alarms" en el manifiesto. Además, para recuperar las sugerencias de la extensión desde una ubicación alojada de forma remota, debes solicitar el permiso de host:

manifest.json:

{
  ...
  "permissions": ["storage"],
  "permissions": ["storage", "alarms"],
  "host_permissions": ["https://extension-tips.glitch.me/*"],
}

La extensión recuperará todas las sugerencias, elegirá una al azar y la guardará en el almacenamiento. Crearemos una alarma que se activará una vez al día para actualizar la sugerencia. Las alarmas no se guardan cuando cierras Chrome. Por lo tanto, debemos comprobar si la alarma existe y crearla si no existe.

sw-tips.js:

// Fetch tip & save in storage
const updateTip = async () => {
  const response = await fetch('https://extension-tips.glitch.me/tips.json');
  const tips = await response.json();
  const randomIndex = Math.floor(Math.random() * tips.length);
  return chrome.storage.local.set({ tip: tips[randomIndex] });
};

const ALARM_NAME = 'tip';

// Check if alarm exists to avoid resetting the timer.
// The alarm might be removed when the browser session restarts.
async function createAlarm() {
  const alarm = await chrome.alarms.get(ALARM_NAME);
  if (typeof alarm === 'undefined') {
    chrome.alarms.create(ALARM_NAME, {
      delayInMinutes: 1,
      periodInMinutes: 1440
    });
    updateTip();
  }
}

createAlarm();

// Update tip once a day
chrome.alarms.onAlarm.addListener(updateTip);

Paso 7: Comunícate con otros contextos

Las extensiones usan secuencias de comandos del contenido para leer y modificar el contenido de la página. Cuando un usuario visita una página de referencia de la API de Chrome, la secuencia de comandos del contenido de la extensión actualizará la página a primera hora. Envía un mensaje para solicitarle la propina del día al service worker.

Comienza por declarar la secuencia de comandos de contenido en el manifiesto y agrega el patrón de coincidencia correspondiente a la documentación de referencia de la API de Chrome.

manifest.json:

{
  ...
  "content_scripts": [
    {
      "matches": ["https://developer.chrome.com/docs/extensions/reference/*"],
      "js": ["content.js"]
    }
  ]
}

Crea un archivo de contenido nuevo. El siguiente código envía un mensaje al trabajador de servicio para solicitar la propina. A continuación, agrega un botón que abrirá una ventana emergente con la sugerencia de la extensión. Este código usa la nueva API de popover de la plataforma web.

content.js:

(async () => {
  // Sends a message to the service worker and receives a tip in response
  const { tip } = await chrome.runtime.sendMessage({ greeting: 'tip' });

  const nav = document.querySelector('.upper-tabs > nav');
  
  const tipWidget = createDomElement(`
    <button type="button" popovertarget="tip-popover" popovertargetaction="show" style="padding: 0 12px; height: 36px;">
      <span style="display: block; font: var(--devsite-link-font,500 14px/20px var(--devsite-primary-font-family));">Tip</span>
    </button>
  `);

  const popover = createDomElement(
    `<div id='tip-popover' popover style="margin: auto;">${tip}</div>`
  );

  document.body.append(popover);
  nav.append(tipWidget);
})();

function createDomElement(html) {
  const dom = new DOMParser().parseFromString(html, 'text/html');
  return dom.body.firstElementChild;
}

El último paso es agregar un controlador de mensajes a nuestro servicio de trabajador que envíe una respuesta a la secuencia de comandos de contenido con la sugerencia diaria.

sw-tips.js:

...
// Send tip to content script via messaging
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.greeting === 'tip') {
    chrome.storage.local.get('tip').then(sendResponse);
    return true;
  }
});

Prueba que funcione

Verifica que la estructura de archivos de tu proyecto se vea de la siguiente manera:

El contenido de la carpeta de la extensión: carpeta de imágenes, manifest.json, service-worker.js, sw-omnibox.js, sw-tips.js y content.js

Carga tu extensión de forma local

Para cargar una extensión sin empaquetar en modo de desarrollador, sigue los pasos que se indican en Hello World.

Cómo abrir una página de referencia

  1. Ingresa la palabra clave “api” en la barra de direcciones del navegador.
  2. Presiona "Tab" o "espacio".
  3. Ingresa el nombre completo de la API.
    • O bien, elige una opción de una lista de búsquedas anteriores.
  4. Se abrirá una página nueva en la página de referencia de la API de Chrome.

Se verá de la siguiente manera:

Referencia rápida de la API para abrir la referencia de la API del entorno de ejecución
Extensión de API rápida que abre la API de Runtime.

Abre la sugerencia del día

Haz clic en el botón Sugerencia ubicado en la barra de navegación para abrir la sugerencia de la extensión.

Cómo abrir la sugerencia diaria en
Extensión de API rápida que abre la sugerencia del día.

🎯 Mejoras potenciales

En función de lo que aprendiste hoy, intenta lograr cualquiera de los siguientes objetivos:

  • Explora otra forma de implementar las sugerencias del cuadro multifunción.
  • Crea tu propio diálogo modal personalizado para mostrar la sugerencia de la extensión.
  • Abre una página adicional a las páginas de API de referencia de extensiones web de MDN.

¡Sigue creando!

¡Felicitaciones por terminar este instructivo! 🎉 Continúa mejorando tus habilidades completando otros instructivos para principiantes:

Extensión Qué aprenderás
Tiempo de lectura Para insertar un elemento en un conjunto específico de páginas automáticamente.
Administrador de pestañas Crear una ventana emergente que administre las pestañas del navegador
Modo sin distracciones Para ejecutar código en la página actual después de hacer clic en la acción de extensión.

Sigue explorando

Para continuar con tu ruta de aprendizaje de los trabajadores del servicio de extensión, te recomendamos que explores los siguientes artículos: