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 dotyczącymi zawartoś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 usługi o konieczności zaktualizowania ikony działania na tej stronie.
Dostępne są 2 interfejsy API do przekazywania wiadomości: jeden do jednorazowych żądań, a drugi, 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 uzyskać 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ą obiekt Promise, który jest rozwiązywany 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 funkcję sendResponse bez żadnych parametrów, w odpowiedzi otrzymasz wartość null.
Aby wysłać odpowiedź asynchronicznie, masz 2 możliwości: zwrócić true lub zwrócić obietnicę.
Wróćtrue
Aby odpowiedzieć asynchronicznie za pomocą sendResponse(), zwróć z funkcji nasłuchującej zdarzenia literał true
(nie tylko wartość prawdziwą). Dzięki temu kanał wiadomości pozostanie otwarty dla drugiej strony, dopóki nie zostanie wywołana funkcja sendResponse, co umożliwi jej późniejsze wywołanie.
Zwracanie obietnicy
Od Chrome w wersji 144 możesz zwracać obietnicę z funkcji nasłuchującej wiadomości, aby odpowiadać asynchronicznie. Jeśli obietnica zostanie spełniona, jej 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 zwracania obietnicy, która może zostać spełniona 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ć słuchacza 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: async pułapki funkcji
Pamiętaj, że funkcja async jako odbiorca zawsze zwraca obietnicę, nawet bez instrukcji return. Jeśli odbiornik async nie zwróci wartości, jego obietnica zostanie niejawnie rozwiązana jako undefined, a wartość null zostanie wysłana jako odpowiedź do nadawcy. Może to powodować nieoczekiwane działanie, gdy jest wielu odbiorcó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
Od Chrome 144, jeśli odbiornik onMessage zgłosi błąd (synchronicznie lub asynchronicznie, zwracając obietnicę, która zostanie odrzucona), obietnica zwrócona przez sendMessage() w nadawcy zostanie odrzucona z komunikatem o błędzie.
Może się to też zdarzyć, jeśli odbiorca próbuje zwrócić odpowiedź, której nie można zserializować do formatu JSON bez przechwycenia wynikającego z tego błędu TypeError.
Jeśli odbiorca zwróci obietnicę, która zostanie odrzucona, musi odrzucić ją za pomocą instancji Error, aby nadawca otrzymał ten komunikat o błędzie. Jeśli obietnica zostanie odrzucona z inną wartością (np. null lub undefined), sendMessage() zostanie odrzucona z ogólnym komunikatem o błędzie.
Jeśli dla onMessage zarejestrowanych jest wielu odbiorców, tylko pierwszy odbiorca, który odpowie, odrzuci lub zgłosi błąd, wpłynie na nadawcę. Wszyscy pozostali odbiorcy zostaną uruchomieni, ale ich wyniki zostaną zignorowane.
Przykłady
Jeśli odbiorca zwróci obietnicę, która zostanie odrzucona, 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 odbiornik odpowie wartością, której nie można serializować, 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 odbiorca zgłosi błąd synchronicznie, zanim zareaguje jakikolwiek inny odbiorca, obietnica sendMessage() odbiorcy 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 z odbiorców 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!');
});
Połączenia długotrwałe
Aby utworzyć kanał przekazywania wiadomości wielokrotnego użytku o długim czasie życia, wywołaj:
runtime.connect()– przekazywanie wiadomości ze skryptu treści na stronę rozszerzenia;tabs.connect()– do przekazywania wiadomości ze strony rozszerzenia do skryptu treści.
Możesz nazwać kanał, przekazując parametr options z kluczem name, aby odróżnić różne typy połączeń:
const port = chrome.runtime.connect({name: "example"});
Jednym z możliwych zastosowań długotrwałego połączenia jest rozszerzenie do automatycznego wypełniania formularzy. Skrypt treści może otworzyć kanał na stronę rozszerzenia dla konkretnego logowania i wysłać do rozszerzenia wiadomość dotyczącą każdego elementu wejściowego na stronie, aby poprosić o dane formularza do wypełnienia. Udostępnione 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 za pomocą tego połączenia.
Aby otworzyć kanał ze skryptu treści oraz wysyłać i odbierać 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.onConnect. Gdy inna część rozszerzenia wywoła connect(), aktywuje to zdarzenie i obiekt runtime.Port. Kod do odpowiadania 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."});
}
});
});
Publikacja w odcinkach
W Chrome interfejsy API do przekazywania wiadomości używają serializacji JSON. W przeciwieństwie do innych przeglądarek, które implementują te same interfejsy API za pomocą algorytmu klonowania strukturalnego.
Oznacza to, że wiadomość (i odpowiedzi udzielone przez odbiorców) może zawierać dowolną JSON.stringify()
prawidłową wartość. Inne wartości zostaną przekształcone w wartości, które można serializować (w szczególności undefined zostanie zserializowana jako null);
Limity rozmiaru wiadomości
Maksymalny rozmiar wiadomości to 64 MiB.
Czas życia portu
Porty są przeznaczone do dwukierunkowej komunikacji między różnymi częściami rozszerzenia. Gdy część rozszerzenia wywołuje 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() powoduje wywołanie zdarzenia runtime.onConnect w przypadku każdej ramki na karcie. Podobnie jeśli wywoływana jest funkcja
runtime.connect(), zdarzenie onConnect może być uruchamiane raz na każdą klatkę w procesie rozszerzenia.
Może Cię interesować, 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. Może to być spowodowane:
- Po drugiej stronie nie ma detektorów dla
runtime.onConnect. - Karta zawierająca port jest zwalniana (np. jeśli nastąpi przejście do innej karty).
- Ramka, w której wywołano funkcję
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 połączenieconnect()powoduje wiele portów po stronie odbiorcy, adisconnect()jest wywoływane na dowolnym z tych portów, zdarzenieonDisconnectjest wywoływane tylko na porcie wysyłającym, a nie na innych portach.
Przesyłanie wiadomości między rozszerzeniami
Oprócz wysyłania wiadomości między różnymi komponentami rozszerzenia możesz używać interfejsu Messaging API do komunikowania się z innymi rozszerzeniami. Dzięki temu możesz udostępnić publiczny interfejs API, z którego mogą korzystać 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żdego 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ę skontaktować, 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, w pliku manifest.json określ, z których witryn chcesz zezwolić na wysyłanie wiadomości, używając klucza manifestu "externally_connectable". Na przykład:
manifest.json
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
Udostępnia to interfejs Messaging API każdej stronie, która pasuje do określonych przez Ciebie wzorców adresów URL. Wzorzec adresu URL musi zawierać co najmniej domenę drugiego poziomu, co oznacza, że wzorce nazw hostów, takie jak „*”, „*.com”, „*.co.uk” i „*.appspot.com”, nie są obsługiwane. Możesz użyć
<all_urls>, aby uzyskać dostęp do wszystkich domen.
Użyj interfejsów API runtime.sendMessage() lub runtime.connect(), aby wysłać wiadomość do konkretnego rozszerzenia. 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 runtime.onMessageExternal lub runtime.onConnectExternal, tak jak w przypadku komunikacji 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 wiadomości natywnych. Więcej informacji o tej funkcji znajdziesz w artykule Wiadomości natywne.
Bezpieczeństwo
Oto kilka kwestii związanych z bezpieczeństwem, które warto wziąć pod uwagę w przypadku wiadomości.
Skrypty dotyczące zawartości są mniej wiarygodne
Skrypty treści są mniej wiarygodne niż proces roboczy usługi 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ły zostać utworzone przez osobę przeprowadzającą atak, i sprawdź oraz oczyść wszystkie dane wejściowe. Załóż, że wszystkie dane wysyłane do skryptu treści mogą wyciec na stronę internetową. Ograniczanie zakresu działań uprzywilejowanych, które mogą być wywoływane przez wiadomości otrzymywane ze skryptów treści.
Cross-site scripting
Zadbaj o to, aby Twoje skrypty były chronione przed skryptami działającymi na różnych stronach. Podczas odbierania danych z niezaufanego źródła, np. z danych wejściowych użytkownika, innych witryn za pomocą skryptu treści lub interfejsu API, należy unikać interpretowania ich jako kodu HTML lub używania ich w sposób, który mógłby umożliwić uruchomienie nieoczekiwanego kodu.
W miarę możliwości 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 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; });