Interfejsy API do obsługi wiadomości umożliwiają komunikację między różnymi skryptami działającymi w kontekstach powiązanych z rozszerzeniem. Obejmuje to komunikację między skryptem service worker, stronami chrome-extension://i skryptami treści. Na przykład rozszerzenie czytnika RSS może używać skryptów treści do wykrywania obecności kanału RSS na stronie, a następnie powiadamiać skrypt service worker o konieczności zaktualizowania ikony działania na tej stronie.
Dostępne są 2 interfejsy API do przesyłania wiadomości: jeden do jednorazowych żądań i bardziej złożony do długotrwałych połączeń, które umożliwiają wysyłanie wielu wiadomości.
Informacje o wysyłaniu wiadomości między rozszerzeniami znajdziesz w sekcji Wiadomości między rozszerzeniami.
Żądania jednorazowe
Aby wysłać pojedynczą wiadomość do innej części rozszerzenia i opcjonalnie otrzymać odpowiedź, wywołaj funkcję runtime.sendMessage() lub tabs.sendMessage().
Te metody umożliwiają wysyłanie jednorazowej wiadomości, którą można serializować do formatu JSON, ze skryptu treści do rozszerzenia lub z rozszerzenia do skryptu treści. Oba interfejsy API zwracają obietnicę, która jest rozwiązywana w odpowiedzi podanej przez odbiorcę.
Wysyłanie żądania ze skryptu treści wygląda tak:
content-script.js:
(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();
Odpowiedzi
Aby nasłuchiwać wiadomości, użyj zdarzenia 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');
Gdy wywoływany jest detektor zdarzeń, jako trzeci parametr przekazywana jest funkcja sendResponse. Jest to funkcja, którą można wywołać, aby podać odpowiedź. Domyślnie wywołanie zwrotne sendResponse musi być wywoływane synchronicznie.
Jeśli wywołasz sendResponse bez żadnych parametrów, jako odpowiedź zostanie wysłana wartość null.
Aby wysłać odpowiedź asynchronicznie, masz 2 możliwości: zwrócenie wartości true lub zwrócenie obietnicy.
Zwróć wartość true
Aby odpowiedzieć asynchronicznie za pomocą funkcji sendResponse(), zwróć literał true (nie tylko wartość prawdziwą) z detektora zdarzeń. Spowoduje to, że kanał wiadomości pozostanie otwarty dla drugiej strony do momentu wywołania funkcji sendResponse, co umożliwi jej wywołanie później.
Zwróć obietnicę
W Chrome w wersji 148 lub nowszej możesz zwrócić obietnicę z detektora wiadomości, aby odpowiedzieć asynchronicznie. Ta aktualizacja jest wdrażana stopniowo, więc może się okazać, że nie jest jeszcze dostępna w przeglądarkach wszystkich użytkowników. Upewnij się, że Twoje rozszerzenie może obsługiwać sytuację, w której ta funkcja jest włączona lub wyłączona. Używanie return true; będzie nadal działać w przypadku odpowiedzi asynchronicznych, niezależnie od tego, czy ta funkcja jest włączona czy nie.
Jeśli obietnica zostanie rozwiązana, jej rozwiązana wartość zostanie wysłana jako odpowiedź.
Jeśli obietnica zostanie odrzucona, wywołanie sendMessage() nadawcy zostanie odrzucone z komunikatem o błędzie. Więcej informacji i przykładów znajdziesz w sekcji
Obsługa błędów.
Przykład, który pokazuje zwracanie obietnicy, która może zostać rozwiązana lub odrzucona:
// 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);
Możesz też zadeklarować detektor jako async, aby zwrócić obietnicę:
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};
});
Zwracanie obietnicy: pułapki funkcji async
Pamiętaj, że funkcja async jako detektor zawsze zwraca obietnicę, nawet bez instrukcji return. Jeśli detektor async nie zwraca wartości, jego obietnica jest niejawnie rozwiązywana jako undefined, a do nadawcy jest wysyłana odpowiedź null. Może to powodować nieoczekiwane działanie, gdy jest wiele detektorów:
// 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');
});
});
Obsługa błędów
W Chrome w wersji 146 lub nowszej, jeśli detektor onMessage zgłosi błąd (synchronicznie lub asynchronicznie przez zwrócenie obietnicy, która zostanie odrzucona), obietnica zwrócona przez sendMessage() w nadawcy zostanie odrzucona z komunikatem o błędzie. Ta aktualizacja jest wdrażana stopniowo, więc może się okazać, że nie jest jeszcze dostępna w przeglądarkach wszystkich użytkowników. Upewnij się, że Twoje rozszerzenie może obsługiwać sytuację, w której ta zmiana jest włączona lub wyłączona.
Jeśli detektor próbuje zwrócić odpowiedź, której nie można
serializować (bez przechwytywania wynikowego TypeError), zostanie to
również uznane za zgłoszenie błędu przez detektor.
Jeśli detektor zwraca obietnicę, która zostanie odrzucona, musi zostać odrzucona z instancją Error, aby nadawca otrzymał ten komunikat o błędzie. Jeśli obietnica zostanie odrzucona z inną wartością (np. null lub undefined), funkcja sendMessage() zostanie odrzucona z ogólnym komunikatem o błędzie.
Jeśli dla onMessage zarejestrowanych jest wiele detektorów, tylko pierwszy detektor, który odpowie, odrzuci lub zgłosi błąd, wpłynie na nadawcę. Wszystkie inne detektory będą działać, ale ich wyniki zostaną zignorowane.
Przykłady
Jeśli detektor zwraca obietnicę, która zostanie odrzucona, funkcja sendMessage() zostanie odrzucona:
// 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'));
});
Jeśli detektor odpowiada wartością, której nie można serializować, funkcja sendMessage() zostanie odrzucona:
// 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
});
Jeśli detektor zgłosi błąd synchronicznie, zanim odpowie jakikolwiek inny detektor, obietnica sendMessage() detektora zostanie odrzucona:
// 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!');
});
Jeśli jednak jeden detektor odpowie, zanim inny zgłosi błąd, funkcja sendMessage() zakończy się powodzeniem:
// 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!');
});
Długotrwałe połączenia
Aby utworzyć kanał do przesyłania wiadomości, który można ponownie wykorzystać, wywołaj:
runtime.connect()– aby przesyłać wiadomości ze skryptu treści na stronę rozszerzenia.tabs.connect()– aby przesyłać wiadomości ze strony rozszerzenia do skryptu treści.
Możesz nazwać swój kanał, przekazując parametr opcji z kluczem name, aby odróżnić różne typy połączeń:
const port = chrome.runtime.connect({name: "example"});
Jednym z potencjalnych przypadków użycia długotrwałego połączenia jest rozszerzenie do automatycznego wypełniania formularzy. Skrypt treści może otworzyć kanał na stronie rozszerzenia na potrzeby konkretnego logowania i wysłać do rozszerzenia wiadomość z prośbą o dane formularza do wypełnienia dla każdego elementu wejściowego na stronie. Wspólne połączenie umożliwia rozszerzeniu udostępnianie stanu między komponentami rozszerzenia.
Podczas nawiązywania połączenia każda strona otrzymuje obiekt runtime.Port do
wysyłania i odbierania wiadomości przez to połączenie.
Aby otworzyć kanał ze skryptu treści oraz wysyłać i nasłuchiwać wiadomości, użyj tego kodu:
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"});
Aby wysłać żądanie z rozszerzenia do skryptu treści, zastąp wywołanie runtime.connect()
w poprzednim przykładzie wywołaniem tabs.connect().
Aby obsługiwać połączenia przychodzące ze skryptu treści lub strony rozszerzenia, skonfiguruj detektor zdarzeń runtime.onConnectset
up a. Gdy inna część rozszerzenia wywoła connect(), aktywuje to zdarzenie i obiekt.runtime.Port Kod do reagowania na połączenia przychodzące wygląda tak:
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."});
}
});
});
Serializacja
W Chrome interfejsy API do obsługi wiadomości używają serializacji JSON. Warto zauważyć, że różni się to od innych przeglądarek, które implementują te same interfejsy API za pomocą algorytmu klonowania strukturalnego.
Oznacza to, że wiadomość (i odpowiedzi podane przez odbiorców) może zawierać dowolną
prawidłową
JSON.stringify()
wartość. Inne wartości zostaną przekształcone w wartości, które można serializować (w szczególności undefined zostanie serializowane jako null).
Limity rozmiaru wiadomości
Maksymalny rozmiar wiadomości to 64 MiB.
Czas życia portu
Porty są zaprojektowane jako dwukierunkowy mechanizm komunikacji między różnymi częściami rozszerzenia. Gdy część rozszerzenia wywoła
tabs.connect(), runtime.connect() lub
runtime.connectNative(), tworzy
port, który może natychmiast wysyłać wiadomości za pomocą
postMessage().
Jeśli karta zawiera wiele ramek, wywołanie tabs.connect() spowoduje wywołanie
zdarzenia runtime.onConnect raz dla każdej ramki na karcie. Podobnie, jeśli
runtime.connect() zostanie wywołana, zdarzenie onConnect może zostać wywołane raz dla każdej
ramki w procesie rozszerzenia.
Możesz chcieć dowiedzieć się, kiedy połączenie zostanie zamknięte, np. jeśli utrzymujesz oddzielne stany dla każdego otwartego portu. Aby to zrobić, nasłuchuj zdarzenia runtime.Port.onDisconnect. To zdarzenie jest wywoływane, gdy po drugiej stronie kanału nie ma prawidłowych portów, co może być spowodowane jedną z tych przyczyn:
- Po drugiej stronie nie ma detektorów
runtime.onConnect. - Karta zawierająca port została zwolniona (np. jeśli karta została przeniesiona).
- Ramka, w której wywołano
connect(), została zwolniona. - Wszystkie ramki, które otrzymały port (za pomocą
runtime.onConnect), zostały zwolnione. runtime.Port.disconnect()jest wywoływana przez drugą stronę. Jeśli wywołanieconnect()spowoduje utworzenie wielu portów po stronie odbiorcy, adisconnect()zostanie wywołane w przypadku dowolnego z tych portów, zdarzenieonDisconnectzostanie wywołane tylko w porcie wysyłającym, a nie w innych portach.
Wiadomości między rozszerzeniami
Oprócz wysyłania wiadomości między różnymi komponentami rozszerzenia możesz używać interfejsu API do obsługi wiadomości, aby komunikować się z innymi rozszerzeniami. Umożliwia to udostępnianie publicznego interfejsu API do użytku przez inne rozszerzenia.
Aby nasłuchiwać przychodzących żądań i połączeń z innych rozszerzeń, użyj metod
runtime.onMessageExternal
lub runtime.onConnectExternal. Oto przykład każdej z nich:
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.
});
});
Aby wysłać wiadomość do innego rozszerzenia, przekaż identyfikator rozszerzenia, z którym chcesz się komunikować, w ten sposób:
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(...);
Wysyłanie wiadomości ze stron internetowych
Rozszerzenia mogą też odbierać wiadomości ze stron internetowych i na nie odpowiadać. Aby wysyłać
wiadomości ze strony internetowej do rozszerzenia, określ w pliku manifest.json, z których
witryn chcesz zezwolić na wysyłanie wiadomości, używając
"externally_connectable" klucza manifestu. Na przykład:
manifest.json
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
Udostępnia to interfejs API do obsługi wiadomości na każdej stronie, która pasuje do określonych wzorców.
Aby wysłać
wiadomość do konkretnego rozszerzenia, użyj interfejsów API runtime.sendMessage() lub runtime.connect(). Na przykład:
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);
}
);
}
W rozszerzeniu nasłuchuj wiadomości ze stron internetowych za pomocą interfejsów API
runtime.onMessageExternal lub runtime.onConnectExternal, tak jak w przypadku wiadomości między rozszerzeniami. Oto przykład:
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);
});
Nie można wysłać wiadomości z rozszerzenia do strony internetowej.
Natywne aplikacje do obsługi wiadomości
Rozszerzenia mogą wymieniać wiadomości z aplikacjami natywnymi zarejestrowanymi jako host natywnego przesyłania komunikatów. Więcej informacji o tej funkcji znajdziesz w artykule Natywne aplikacje do obsługi wiadomości.
Bezpieczeństwo
Oto kilka kwestii związanych z bezpieczeństwem w kontekście obsługi wiadomości.
Skrypty treści są mniej wiarygodne
Skrypty treści są mniej wiarygodne niż skrypt service worker rozszerzenia. Na przykład złośliwa strona internetowa może naruszyć proces renderowania, w którym działają skrypty treści. Załóż, że wiadomości ze skryptu treści mogą być spreparowane przez atakującego, i sprawdzaj oraz oczyszczaj wszystkie dane wejściowe. Załóż, że wszystkie dane wysyłane do skryptu treści mogą wyciec na stronę internetową. Ogranicz zakres działań uprzywilejowanych, które mogą być wywoływane przez wiadomości otrzymywane ze skryptów treści.
Cross-site scripting
Zadbaj o ochronę skryptów przed atakami typu cross-site scripting. Podczas odbierania danych z niezaufanego źródła, takiego jak dane wejściowe użytkownika, inne witryny za pomocą skryptu treści lub interfejsu API, unikaj interpretowania ich jako HTML lub używania ich w sposób, który mógłby umożliwić uruchomienie nieoczekiwanego kodu.
Jeśli to możliwe, używaj interfejsów API, które nie uruchamiają skryptów:
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; });
Unikaj używania tych metod, które sprawiają, że rozszerzenie jest podatne na ataki:
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; });