Le API di messaggistica ti consentono di comunicare tra diversi script in esecuzione in contesti associati alla tua estensione. Ciò include la comunicazione tra il service worker, le pagine chrome-extension://e gli script di contenuti. Ad esempio, un'estensione di lettura di feed RSS potrebbe utilizzare script di contenuti per rilevare la presenza di un feed RSS in una pagina, quindi notificare al service worker di aggiornare l'icona dell'azione per quella pagina.
Esistono due API di passaggio dei messaggi: una per le richieste una tantum e una più complessa per le connessioni a lunga durata che consentono l'invio di più messaggi.
Per informazioni sull'invio di messaggi tra le estensioni, consulta la sezione Messaggi tra estensioni.
Richieste una tantum
Per inviare un singolo messaggio a un'altra parte dell'estensione e, facoltativamente, ricevere una
risposta, chiama runtime.sendMessage() o tabs.sendMessage().
Questi metodi consentono di inviare un messaggio serializzabile in formato JSON una tantum da uno script di contenuti all'estensione o dall'estensione a uno script di contenuti. Entrambe le API restituiscono una promessa che viene risolta nella risposta fornita da un destinatario.
L'invio di una richiesta da uno script di contenuti ha il seguente aspetto:
content-script.js:
(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();
Risposte
Per rimanere in ascolto di un messaggio, utilizza l'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');
Quando viene chiamato il listener di eventi, viene passata una funzione sendResponse come terzo parametro. Si tratta di una funzione che può essere chiamata per fornire una risposta. Per impostazione predefinita, il callback sendResponse deve essere chiamato in modo sincrono.
Se chiami sendResponse senza parametri, viene inviato null come risposta.
Per inviare una risposta in modo asincrono, hai due opzioni: restituire true o restituire una promessa.
Restituire true
Per rispondere in modo asincrono utilizzando sendResponse(), restituisci un valore letterale true (non solo un valore truthy) dal listener di eventi. In questo modo, il canale di messaggi rimane aperto all'altra estremità finché non viene chiamato sendResponse, consentendoti di chiamarlo in un secondo momento.
Restituire una promessa
A partire da Chrome 146, puoi restituire una promessa da un listener di messaggi per rispondere in modo asincrono. Questo aggiornamento viene implementato gradualmente, quindi potresti scoprire che non è ancora disponibile nei browser di tutti gli utenti. Devi assicurarti che la tua estensione possa gestire l'attivazione o la disattivazione di questa funzionalità. L'utilizzo di return true; continuerà a funzionare per le risposte asincrone, indipendentemente dall'attivazione o meno di questa funzionalità.
Se la promessa viene risolta, il valore risolto viene inviato come risposta.
Se la promessa viene rifiutata, la chiamata sendMessage() del mittente verrà rifiutata con il messaggio di errore. Per maggiori dettagli ed esempi, consulta la sezione
Gestione degli errori.
Un esempio che mostra la restituzione di una promessa che potrebbe essere risolta o rifiutata:
// 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);
Puoi anche dichiarare un listener come async per restituire una promessa:
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};
});
Restituire una promessa: insidie della funzione async
Tieni presente che una funzione async come listener restituirà sempre una promessa, anche senza un'istruzione return. Se un listener async non restituisce un valore, la sua promessa viene risolta implicitamente in undefined e null viene inviato come risposta al mittente. Questo può causare un comportamento imprevisto quando sono presenti più listener:
// 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');
});
});
Gestione degli errori
A partire da Chrome 146, se un listener onMessage genera un errore (in modo sincrono o asincrono restituendo una promessa che viene rifiutata), la promessa restituita da sendMessage() nel mittente verrà rifiutata con il messaggio di errore. Questo aggiornamento viene implementato gradualmente, quindi potresti scoprire che questa modifica non è ancora effettiva nei browser di tutti gli utenti. Devi assicurarti che la tua estensione possa gestire l'attivazione o la disattivazione di questa modifica.
Se un listener tenta di restituire una risposta che non può essere
serializzata (senza intercettare il TypeError risultante), questo
verrà considerato come se il listener generasse un errore.
Se un listener restituisce una promessa che viene rifiutata, deve essere rifiutata con un'istanza Error affinché il mittente riceva il messaggio di errore. Se la promessa viene rifiutata con qualsiasi altro valore (ad esempio null o undefined), sendMessage() verrà rifiutata con un messaggio di errore generico.
Se sono registrati più listener per onMessage, solo il primo listener che risponde, rifiuta o genera un errore influirà sul mittente; tutti gli altri listener verranno eseguiti, ma i loro risultati verranno ignorati.
Esempi
Se un listener restituisce una promessa che viene rifiutata, sendMessage() viene rifiutata:
// 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'));
});
Se un listener risponde con un valore che non può essere serializzato, sendMessage() viene rifiutata:
// 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
});
Se un listener genera un errore in modo sincrono prima che qualsiasi altro listener risponda, la promessa sendMessage() del listener viene rifiutata:
// 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!');
});
Tuttavia, se un listener risponde prima che un altro generi un errore, sendMessage() ha esito positivo:
// 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!');
});
Connessioni a lunga durata
Per creare un canale di passaggio dei messaggi a lunga durata riutilizzabile, chiama:
runtime.connect()per passare i messaggi da uno script di contenuti a una pagina dell'estensionetabs.connect()per passare i messaggi da una pagina dell'estensione a uno script di contenuti.
Puoi assegnare un nome al canale passando un parametro options con una chiave name per distinguere tra diversi tipi di connessioni:
const port = chrome.runtime.connect({name: "example"});
Un potenziale caso d'uso per una connessione a lunga durata è un'estensione di compilazione automatica dei moduli. Lo script di contenuti potrebbe aprire un canale alla pagina dell'estensione per un accesso specifico e inviare un messaggio all'estensione per ogni elemento di input della pagina per richiedere i dati del modulo da compilare. La connessione condivisa consente all'estensione di condividere lo stato tra i componenti dell'estensione.
Quando viene stabilita una connessione, a ogni estremità viene assegnato un oggetto runtime.Port per
l'invio e la ricezione di messaggi tramite quella connessione.
Utilizza il seguente codice per aprire un canale da uno script di contenuti, inviare e ascoltare i messaggi:
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"});
Per inviare una richiesta dall'estensione a uno script di contenuti, sostituisci la chiamata a runtime.connect()
nell'esempio precedente con tabs.connect().
Per gestire le connessioni in entrata per uno script di contenuti o una pagina dell'estensione, set
up a runtime.onConnect listener di eventi. Quando un'altra parte della tua
estensione chiama connect(), attiva questo evento e l'oggetto runtime.Port. Il codice per rispondere alle connessioni in entrata ha il seguente aspetto:
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."});
}
});
});
Serializzazione
In Chrome, le API di passaggio dei messaggi utilizzano la serializzazione JSON. In particolare, questo è diverso dagli altri browser che implementano le stesse API con l' algoritmo di clonazione strutturata.
Ciò significa che un messaggio (e le risposte fornite dai destinatari) può contenere qualsiasi
valore
JSON.stringify()
valido. Gli altri valori verranno forzati in valori serializzabili (in particolare undefined verrà serializzato come null);
Limiti di dimensione dei messaggi
La dimensione massima di un messaggio è di 64 MiB.
Durata della porta
Le porte sono progettate come meccanismo di comunicazione bidirezionale tra diverse parti di un'estensione. Quando una parte di un'estensione chiama
tabs.connect(), runtime.connect() o
runtime.connectNative(), crea una
porta che può inviare immediatamente messaggi utilizzando
postMessage().
Se in una scheda sono presenti più frame, la chiamata a tabs.connect() richiama
l'evento runtime.onConnect una volta per ogni frame della scheda. Allo stesso modo, se
runtime.connect() viene chiamato, l'evento onConnect può essere attivato una volta per ogni
frame nel processo di estensione.
Potresti voler scoprire quando una connessione viene chiusa, ad esempio se gestisci stati separati per ogni porta aperta. Per farlo, ascolta l'evento runtime.Port.onDisconnect. Questo evento viene attivato quando non sono presenti porte valide all'altra estremità del canale, il che può avere una delle seguenti cause:
- Non sono presenti listener per
runtime.onConnectall'altra estremità. - La scheda contenente la porta viene scaricata (ad esempio, se la scheda viene spostata).
- Il frame in cui è stato chiamato
connect()è stato scaricato. - Tutti i frame che hanno ricevuto la porta (tramite
runtime.onConnect) sono stati scaricati. runtime.Port.disconnect()viene chiamato dall'altra estremità. Se unaconnect()chiamata genera più porte all'estremità del destinatario edisconnect()viene chiamata su una di queste porte, l'eventoonDisconnectviene attivato solo sulla porta di invio, non sulle altre porte.
Messaggi tra estensioni
Oltre a inviare messaggi tra diversi componenti dell'estensione, puoi utilizzare l'API di messaggistica per comunicare con altre estensioni. In questo modo, puoi esporre un'API pubblica che altre estensioni possono utilizzare.
Per rimanere in ascolto delle richieste e delle connessioni in entrata da altre estensioni, utilizza i
runtime.onMessageExternal
o runtime.onConnectExternal metodi. Ecco un esempio di ciascuno:
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.
});
});
Per inviare un messaggio a un'altra estensione, passa l'ID dell'estensione con cui vuoi comunicare come segue:
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(...);
Inviare messaggi dalle pagine web
Le estensioni possono anche ricevere messaggi dalle pagine web e rispondere. Per inviare
messaggi da una pagina web a un'estensione, specifica in manifest.json i
siti web da cui vuoi consentire i messaggi utilizzando la
"externally_connectable" chiave del manifest. Ad esempio:
manifest.json
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
In questo modo, l'API di messaggistica viene esposta a qualsiasi pagina che corrisponda ai pattern di corrispondenza specificati.
Utilizza le API runtime.sendMessage() o runtime.connect() per inviare
un messaggio a un'estensione specifica. Ad esempio:
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);
}
);
}
Dall'estensione, ascolta i messaggi dalle pagine web utilizzando le
runtime.onMessageExternal o runtime.onConnectExternal
API, come nella messaggistica tra estensioni. Ecco un esempio:
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);
});
Non è possibile inviare un messaggio da un'estensione a una pagina web.
Messaggi nativi
Le estensioni possono scambiare messaggi con applicazioni native registrate come host di messaggistica nativa. Per saperne di più su questa funzionalità, consulta Messaggi nativi.
Considerazioni sulla sicurezza
Ecco alcune considerazioni sulla sicurezza relative alla messaggistica.
Gli script di contenuti sono meno affidabili
Gli script di contenuti sono meno affidabili del service worker dell'estensione. Ad esempio, una pagina web dannosa potrebbe essere in grado di compromettere il processo di rendering che esegue gli script di contenuti. Supponi che i messaggi provenienti da uno script di contenuti possano essere stati creati da un utente malintenzionato e assicurati di convalidare e sanificare tutti gli input. Supponi che tutti i dati inviati allo script di contenuti possano essere divulgati alla pagina web. Limita l'ambito delle azioni privilegiate che possono essere attivate dai messaggi ricevuti dagli script di contenuti.
Cross-site scripting
Assicurati di proteggere gli script dal cross-site scripting. Quando ricevi dati da una fonte non attendibile, come input dell'utente, altri siti web tramite uno script di contenuti o un'API, fai attenzione a non interpretarli come HTML o a utilizzarli in modo da consentire l'esecuzione di codice imprevisto.
Se possibile, utilizza le API che non eseguono script:
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 di utilizzare i seguenti metodi che rendono vulnerabile la tua estensione:
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; });