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, chrome-extension://pages y las 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 service worker para que actualice el ícono de acción de esa página.
Existen dos APIs de paso de mensajes: una para solicitudes únicas y otra más compleja para conexiones duraderas que permiten enviar varios mensajes.
Si quieres obtener información para enviar 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 muestran 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 detectar 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 el tercer parámetro. Esta es una función 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 parámetros, se envía null como respuesta.
Para enviar una respuesta de forma asíncrona, tienes dos opciones: mostrar true o mostrar una promesa.
Devolver true
Para responder de forma asíncrona con sendResponse(), muestra 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 tarde.
Devolver una promesa
A partir de Chrome 146, puedes mostrar una promesa desde un objeto de escucha de mensajes para responder de forma asíncrona. Esta actualización se implementa de forma gradual, por lo que es posible que aún no esté disponible en los navegadores de todos los usuarios. Debes asegurarte de que tu extensión pueda controlar si esta capacidad está habilitada o no. El uso de return true; seguirá funcionando para las respuestas asíncronas, ya sea que esta capacidad esté habilitada o no.
Si se resuelve la promesa, su valor resuelto se envía como respuesta.
Si se rechaza la promesa, se rechazará la llamada sendMessage() del remitente con el mensaje de error. Consulta la sección de
manejo 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};
});
Devolver una promesa: errores comunes de la función async
Ten en cuenta que una función async como objeto de escucha siempre devolverá una promesa, incluso sin una instrucción return. Si un objeto de escucha 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 146, si un objeto de escucha onMessage arroja un error (ya sea de forma síncrona o asíncrona devolviendo una promesa que rechaza), la promesa que muestra sendMessage() en el remitente se rechazará con el mensaje de error. Esta actualización se implementa de forma gradual, por lo que es posible que este cambio aún no sea efectivo en los navegadores de todos los usuarios. Debes asegurarte de que tu extensión pueda controlar si este cambio está habilitado o no.
Si un objeto de escucha intenta devolver una respuesta que no se puede
serializar (sin detectar el TypeError resultante), esto
también se considerará que el objeto de escucha arroja un error.
Si un objeto de escucha devuelve una promesa que rechaza, debe rechazarla con una instancia 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 se ignorarán sus resultados.
Ejemplos
Si un objeto de escucha devuelve una promesa que rechaza, se rechaza sendMessage():
// 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 paso de mensajes duradero y reutilizable, 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 duradera es una extensión de autocompletado de formularios. La secuencia de comandos de contenido podría abrir un canal a la página de extensión para un acceso específico y enviar un mensaje a la extensión para cada elemento de entrada de la página para 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 detectar 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()
en el 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 runtime.Port
objeto. El código para responder a las conexiones entrantes se ve de la siguiente manera:
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 paso de mensajes usan la serialización 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) pueden contener cualquier
valor
JSON.stringify()
válido. Otros valores se forzarán a 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 parte de una extensión llama a
tabs.connect(), runtime.connect() o
runtime.connectNative(), se crea un
puerto que puede enviar mensajes de inmediato con
postMessage().
Si hay varios marcos en una pestaña, llamar a tabs.connect() invoca
el runtime.onConnect evento una vez para cada marco de la pestaña. Del mismo modo, si
runtime.connect() se llama, el evento onConnect puede activarse una vez para cada
marco 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 runtime.Port.onDisconnect evento. Este evento se activa cuando no hay puertos válidos en el otro extremo del canal, lo que puede tener cualquiera de las siguientes causas:
- No hay objetos 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 unaconnect()llamada 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 demás 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
runtime.onMessageExternal
o runtime.onConnectExternal métodos. Aquí tienes 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 mensajes de páginas web y responderlos. Para enviar
mensajes de una página web a una extensión, especifica en tu manifest.json los
sitios web desde los que deseas permitir mensajes con la
"externally_connectable" clave de manifiesto. 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 coincidencia que especifiques.
Usa las APIs 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, detecta mensajes de páginas web con las
runtime.onMessageExternal o runtime.onConnectExternal
APIs como en los mensajes 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 que están registradas como un 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 poner en riesgo el proceso de renderización que ejecuta las secuencias de comandos de contenido. Supón que un atacante podría haber creado mensajes de una secuencia de comandos de contenido y asegúrate de validar y desinfectar todas las entradas. Supón que cualquier dato enviado a la secuencia de comandos de contenido podría filtrarse a la página web. Limita el alcance de las acciones privilegiadas que pueden activar los mensajes recibidos de las secuencias de comandos de contenido.
Secuencia de comandos entre sitios
Asegúrate de proteger tus secuencias de comandos contra las secuencias de comandos entre sitios. Cuando recibas datos de una fuente no confiable, como la entrada del usuario, otros sitios web a través de una secuencia de comandos 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; });