Las APIs de mensajería te permiten comunicarte entre diferentes secuencias de comandos que se ejecutan en contextos asociados con tu extensión. Esto incluye la comunicación entre tu service worker, las páginas chrome-extension://y los secuencias de comandos de contenido. Por ejemplo, una extensión de lector de RSS podría usar secuencias de comandos de contenido para detectar la presencia de un feed RSS en una página y, luego, notificar al trabajador de servicio para que actualice el ícono de acción de esa página.
Existen dos APIs de transmisión de mensajes: una para solicitudes únicas y otra más compleja para conexiones de larga duración que permiten enviar varios mensajes.
Para obtener información sobre el envío de mensajes entre extensiones, consulta la sección Mensajes entre extensiones.
Solicitudes únicas
Para enviar un solo mensaje a otra parte de tu extensión y, de manera opcional, obtener una respuesta, llama a runtime.sendMessage() o tabs.sendMessage().
Estos métodos te permiten enviar un mensaje serializable en JSON único desde una secuencia de comandos de contenido a la extensión, o desde la extensión a una secuencia de comandos de contenido. Ambas APIs devuelven una promesa que se resuelve en la respuesta proporcionada por un destinatario.
El envío de una solicitud desde una secuencia de comandos de contenido se ve de la siguiente manera:
content-script.js:
(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();
Respuestas
Para escuchar un mensaje, usa el evento chrome.runtime.onMessage:
// Event listener
function handleMessages(message, sender, sendResponse) {
if (message !== 'get-status') return;
fetch('https://example.com')
.then((response) => sendResponse({statusCode: response.status}))
// Since `fetch` is asynchronous, must return an explicit `true`
return true;
}
chrome.runtime.onMessage.addListener(handleMessages);
// From the sender's context...
const {statusCode} = await chrome.runtime.sendMessage('get-status');
Cuando se llama al objeto de escucha de eventos, se pasa una función sendResponse como tercer parámetro. Esta es una función a la que se puede llamar para proporcionar una respuesta. De forma predeterminada, se debe llamar a la devolución de llamada sendResponse de forma síncrona.
Si llamas a sendResponse sin ningún parámetro, se envía null como respuesta.
Para enviar una respuesta de forma asíncrona, tienes dos opciones: devolver true o devolver una promesa.
Devuelve true
Para responder de forma asíncrona con sendResponse(), devuelve un literal true (no solo un valor verdadero) desde el objeto de escucha de eventos. Si lo haces, el canal de mensajes permanecerá abierto en el otro extremo hasta que se llame a sendResponse, lo que te permitirá llamarlo más adelante.
Devuelve una promesa
A partir de Chrome 144, puedes devolver una promesa desde un objeto de escucha de mensajes para responder de forma asíncrona. Si la promesa se resuelve, su valor resuelto se envía como respuesta.
Si se rechaza la promesa, se rechazará la llamada a sendMessage() del remitente con el mensaje de error. Consulta la sección control de errores para obtener más detalles y ejemplos.
Un ejemplo que muestra cómo devolver una promesa que podría resolverse o rechazarse:
// Event listener
function handleMessages(message, sender, sendResponse) {
// Return a promise that wraps fetch
// If the response is OK, resolve with the status. If it's not OK then reject
// with the network error that prevents the fetch from completing.
return new Promise((resolve, reject) => {
fetch('https://example.com')
.then(response => {
if (!response.ok) {
reject(response);
} else {
resolve(response.status);
}
})
.catch(error => {
reject(error);
});
});
}
chrome.runtime.onMessage.addListener(handleMessages);
También puedes declarar un objeto de escucha como async para devolver una promesa:
chrome.runtime.onMessage.addListener(async function(message, sender) {
const response = await fetch('https://example.com');
if (!response.ok) {
// rejects the promise returned by `async function`.
throw new Error(`Fetch failed: ${response.status}`);
}
// resolves the promise returned by `async function`.
return {statusCode: response.status};
});
Devolución de una promesa: errores comunes de la función async
Ten en cuenta que una función async como listener siempre devolverá una promesa, incluso sin una instrucción return. Si un objeto de escucha de async no devuelve un valor, su promesa se resuelve de forma implícita en undefined y se envía null como respuesta al remitente. Esto puede causar un comportamiento inesperado cuando hay varios objetos de escucha:
// content_script.js
function handleResponse(message) {
// The first listener promise resolves to `undefined` before the second
// listener can respond. When a listener responds with `undefined`, Chrome
// sends null as the response.
console.assert(message === null);
}
function notifyBackgroundPage() {
const sending = chrome.runtime.sendMessage('test');
sending.then(handleResponse);
}
notifyBackgroundPage();
// background.js
chrome.runtime.onMessage.addListener(async (message) => {
// This just causes the function to pause for a millisecond, but the promise
// is *not* returned from the listener so it doesn't act as a response.
await new Promise(resolve => {
setTimeout(resolve, 1, 'OK');
});
// `async` functions always return promises. So once we
// reach here there is an implicit `return undefined;`. Chrome translates
// `undefined` responses to `null`.
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return new Promise((resolve) => {
setTimeout(resolve, 1000, 'response');
});
});
Manejo de errores
A partir de Chrome 144, si un objeto de escucha onMessage arroja un error (ya sea de forma síncrona o asíncrona devolviendo una promesa que se rechaza), la promesa que devuelve sendMessage() en el remitente se rechazará con el mensaje del error.
Esto también puede ocurrir si un objeto de escucha intenta devolver una respuesta que no se puede serializar en formato JSON sin detectar el TypeError resultante.
Si un objeto de escucha devuelve una promesa que se rechaza, debe hacerlo con una instancia de Error para que el remitente reciba ese mensaje de error. Si la promesa se rechaza con cualquier otro valor (como null o undefined), sendMessage() se rechazará con un mensaje de error genérico.
Si se registran varios objetos de escucha para onMessage, solo el primer objeto de escucha que responda, rechace o arroje un error afectará al remitente; todos los demás objetos de escucha se ejecutarán, pero sus resultados se ignorarán.
Ejemplos
Si un objeto de escucha devuelve una promesa que se rechaza, sendMessage() se rechaza:
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "some error"
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return Promise.reject(new Error('some error'));
});
Si un objeto de escucha responde con un valor que no se puede serializar, se rechaza sendMessage():
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "Error: Could not serialize message."
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse(() => {}); // Functions are not serializable
return true; // Keep channel open for async sendResponse
});
Si un objeto de escucha arroja un error de forma síncrona antes de que responda cualquier otro objeto de escucha, se rechaza la promesa sendMessage() del objeto de escucha:
// sender.js
try {
await chrome.runtime.sendMessage('test');
} catch (e) {
console.log(e.message); // "error!"
}
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
throw new Error('error!');
});
Sin embargo, si un objeto de escucha responde antes de que otro arroje un error, sendMessage() se ejecuta correctamente:
// sender.js
const response = await chrome.runtime.sendMessage('test');
console.log(response); // "OK"
// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse('OK');
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
throw new Error('error!');
});
Conexiones duraderas
Para crear un canal de transferencia de mensajes reutilizable y de larga duración, llama a lo siguiente:
runtime.connect()para pasar mensajes de una secuencia de comandos de contenido a una página de extensióntabs.connect()para pasar mensajes de una página de extensión a una secuencia de comandos de contenido.
Puedes nombrar tu canal pasando un parámetro de opciones con una clave name para distinguir entre diferentes tipos de conexiones:
const port = chrome.runtime.connect({name: "example"});
Un posible caso de uso para una conexión de larga duración es una extensión de autocompletado de formularios. La secuencia de comandos de contenido puede abrir un canal a la página de la extensión para un acceso específico y enviar un mensaje a la extensión para cada elemento de entrada de la página y solicitar los datos del formulario que se deben completar. La conexión compartida permite que la extensión comparta el estado entre los componentes de la extensión.
Cuando se establece una conexión, a cada extremo se le asigna un objeto runtime.Port para enviar y recibir mensajes a través de esa conexión.
Usa el siguiente código para abrir un canal desde una secuencia de comandos de contenido, y enviar y escuchar mensajes:
content-script.js:
const port = chrome.runtime.connect({name: "knockknock"});
port.onMessage.addListener(function(msg) {
if (msg.question === "Who's there?") {
port.postMessage({answer: "Madame"});
} else if (msg.question === "Madame who?") {
port.postMessage({answer: "Madame... Bovary"});
}
});
port.postMessage({joke: "Knock knock"});
Para enviar una solicitud desde la extensión a una secuencia de comandos de contenido, reemplaza la llamada a runtime.connect() del ejemplo anterior por tabs.connect().
Para controlar las conexiones entrantes de una secuencia de comandos de contenido o una página de extensión, configura un objeto de escucha de eventos runtime.onConnect. Cuando otra parte de tu extensión llama a connect(), se activa este evento y el objeto runtime.Port. El código para responder a las conexiones entrantes se ve así:
service-worker.js:
chrome.runtime.onConnect.addListener(function(port) {
if (port.name !== "knockknock") {
return;
}
port.onMessage.addListener(function(msg) {
if (msg.joke === "Knock knock") {
port.postMessage({question: "Who's there?"});
} else if (msg.answer === "Madame") {
port.postMessage({question: "Madame who?"});
} else if (msg.answer === "Madame... Bovary") {
port.postMessage({question: "I don't get it."});
}
});
});
Serialización
En Chrome, las APIs de transferencia de mensajes usan la serialización de JSON. En particular, esto es diferente de otros navegadores que implementan las mismas APIs con el algoritmo de clonación estructurada.
Esto significa que un mensaje (y las respuestas proporcionadas por los destinatarios) puede contener cualquier valor de JSON.stringify() válido. Otros valores se convertirán en valores serializables (en particular, undefined se serializará como null).
Límites de tamaño de los mensajes
El tamaño máximo de un mensaje es de 64 MiB.
Duración del puerto
Los puertos están diseñados como un mecanismo de comunicación bidireccional entre diferentes partes de una extensión. Cuando una parte de una extensión llama a tabs.connect(), runtime.connect() o runtime.connectNative(), se crea un Port que puede enviar mensajes de inmediato con postMessage().
Si hay varios marcos en una pestaña, llamar a tabs.connect() invoca el evento runtime.onConnect una vez para cada marco de la pestaña. Del mismo modo, si se llama a runtime.connect(), el evento onConnect se puede activar una vez por cada fotograma del proceso de extensión.
Es posible que desees saber cuándo se cierra una conexión, por ejemplo, si mantienes estados separados para cada puerto abierto. Para ello, escucha el evento runtime.Port.onDisconnect. Este evento se activa cuando no hay puertos válidos en el otro extremo del canal, lo que puede deberse a cualquiera de los siguientes motivos:
- No hay ningún objeto de escucha para
runtime.onConnecten el otro extremo. - Se descarga la pestaña que contiene el puerto (por ejemplo, si se navega por la pestaña).
- Se descargó el marco en el que se llamó a
connect(). - Se descargaron todos los marcos que recibieron el puerto (a través de
runtime.onConnect). runtime.Port.disconnect()es llamado por el otro extremo. Si una llamada aconnect()genera varios puertos en el extremo del receptor y se llama adisconnect()en cualquiera de estos puertos, el eventoonDisconnectsolo se activa en el puerto de envío, no en los otros puertos.
Mensajes entre extensiones
Además de enviar mensajes entre diferentes componentes de tu extensión, puedes usar la API de mensajería para comunicarte con otras extensiones. Esto te permite exponer una API pública para que la usen otras extensiones.
Para detectar solicitudes y conexiones entrantes de otras extensiones, usa los métodos runtime.onMessageExternal o runtime.onConnectExternal. A continuación, se incluye un ejemplo de cada uno:
service-worker.js
// For a single request:
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.id !== allowlistedExtension) {
return; // don't allow this extension access
}
if (request.getTargetData) {
sendResponse({ targetData: targetData });
} else if (request.activateLasers) {
const success = activateLasers();
sendResponse({ activateLasers: success });
}
}
);
// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
port.onMessage.addListener(function(msg) {
// See other examples for sample onMessage handlers.
});
});
Para enviar un mensaje a otra extensión, pasa el ID de la extensión con la que deseas comunicarte de la siguiente manera:
service-worker.js
// The ID of the extension we want to talk to.
const laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// For a minimal request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
function(response) {
if (targetInRange(response.targetData))
chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
}
);
// For a long-lived connection:
const port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);
Envía mensajes desde páginas web
Las extensiones también pueden recibir y responder mensajes de páginas web. Para enviar mensajes desde una página web a una extensión, especifica en tu manifest.json los sitios web desde los que deseas permitir mensajes con la clave de manifiesto "externally_connectable". Por ejemplo:
manifest.json
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
Esto expone la API de mensajería a cualquier página que coincida con los patrones de URL que especifiques. El patrón de URL debe contener al menos un dominio de segundo nivel, es decir, no se admiten patrones de nombres de host como "*", "*.com", "*.co.uk" y "*.appspot.com". Puedes usar <all_urls> para acceder a todos los dominios.
Usa las APIs de runtime.sendMessage() o runtime.connect() para enviar un mensaje a una extensión específica. Por ejemplo:
webpage.js
// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';
// Check if extension is installed
if (chrome && chrome.runtime) {
// Make a request:
chrome.runtime.sendMessage(
editorExtensionId,
{
openUrlInEditor: url
},
(response) => {
if (!response.success) handleError(url);
}
);
}
Desde tu extensión, escucha los mensajes de las páginas web con las APIs de runtime.onMessageExternal o runtime.onConnectExternal, como en la mensajería entre extensiones. Por ejemplo:
service-worker.js
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.url === blocklistedWebsite)
return; // don't allow this web page access
if (request.openUrlInEditor)
openUrl(request.openUrlInEditor);
});
No es posible enviar un mensaje desde una extensión a una página web.
Mensajería nativa
Las extensiones pueden intercambiar mensajes con aplicaciones nativas registradas como host de mensajería nativa. Para obtener más información sobre esta función, consulta Mensajería nativa.
Consideraciones de seguridad
Estas son algunas consideraciones de seguridad relacionadas con la mensajería.
Las secuencias de comandos de contenido son menos confiables
Las secuencias de comandos de contenido son menos confiables que el service worker de la extensión. Por ejemplo, una página web maliciosa podría comprometer el proceso de procesamiento que ejecuta las secuencias de comandos de contenido. Supón que un atacante podría haber creado mensajes desde una secuencia de comandos de contenido y asegúrate de validar y limpiar todas las entradas. Supón que cualquier dato que se envíe al script de contenido podría filtrarse a la página web. Limita el alcance de las acciones privilegiadas que pueden activarse con los mensajes recibidos de las secuencias de comandos de contenido.
Secuencia de comandos entre sitios
Asegúrate de proteger tus secuencias de comandos contra el scripting entre sitios. Cuando recibas datos de una fuente no confiable, como la entrada del usuario, otros sitios web a través de un script de contenido o una API, ten cuidado de no interpretarlos como HTML ni usarlos de una manera que permita que se ejecute código inesperado.
Usa APIs que no ejecuten secuencias de comandos siempre que sea posible:
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // JSON.parse doesn't evaluate the attacker's scripts. const resp = JSON.parse(response.farewell); });
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // innerText does not let the attacker inject HTML elements. document.getElementById("resp").innerText = response.farewell; });
Evita usar los siguientes métodos que hacen que tu extensión sea vulnerable:
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // WARNING! Might be evaluating a malicious script! const resp = eval(`(${response.farewell})`); });
service-worker.js
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) { // WARNING! Might be injecting a malicious script! document.getElementById("resp").innerHTML = response.farewell; });